Repository: omry/omegaconf

Issue:
Validate behavior of overlaying PostGreSQLConfig() over db: DBConfig = MySQLConfig()
```
@dataclass
class Config(DictConfig):
    db: DBConfig = MySQLConfig()
```
Validate that this behavior makes sense, this seems wrong.
```
$ python examples/structured_configs_tutorial/4_config_groups/my_app_with_inheritance.py database=postgresql
...
omegaconf.errors.ValidationError: Invalid type assigned : PostGreSQLConfig is not a subclass of MySQLConfig
```

Discussion:
deepcode-ci-bot[bot]: <img src="https://www.deepcode.ai/icons/green_check.svg" width= "50px" align= "left"/> Congratulations :tada:. DeepCode [analyzed](https://www.deepcode.ai/app/gh/omry/omegaconf/00b940bbeedc47314451300e7f50e6c39856a6ea/omry/omegaconf/802898269387ce62f8dd50b784d894dec60c3a6d/_/%2F/code/?utm_source=gh_review) your code in 2.869 seconds and we found no issues. Enjoy a moment of no bugs :sunny:.

##### 👉 View analysis in [**DeepCode’s Dashboard**](https://www.deepcode.ai/app/gh/omry/omegaconf/00b940bbeedc47314451300e7f50e6c39856a6ea/omry/omegaconf/802898269387ce62f8dd50b784d894dec60c3a6d/_/%2F/code/?utm_source=gh_review)
---


Patch:
diff --git a/news/186.feature b/news/186.feature
new file mode 100644
index 0000000..08dab46
--- /dev/null
+++ b/news/186.feature
@@ -0,0 +1,1 @@
+Standardize exception messages
\ No newline at end of file
diff --git a/noxfile.py b/noxfile.py
index 63142b5..362bd5f 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -50,6 +50,7 @@ def lint(session):
     session.run("mypy", ".", "--strict", silent=True)
     session.run("isort", ".", "--check", silent=True)
     session.run("black", "--check", ".", silent=True)
+    session.run("flake8")
 
 
 @nox.session(python=PYTHON_VERSIONS)
diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py
index 40b045f..436c056 100644
--- a/omegaconf/_utils.py
+++ b/omegaconf/_utils.py
@@ -1,4 +1,7 @@
 import re
+import string
+import sys
+import warnings
 from enum import Enum
 from typing import Any, Dict, List, Match, Optional, Tuple, Type, Union
 
@@ -114,11 +117,7 @@ def get_attr_data(obj: Any) -> Dict[str, Any]:
                         "default must be populated later use '???'".format(name)
                     )
         d[name] = _maybe_wrap(
-            annotated_type=type_,
-            is_optional=is_optional,
-            key=name,
-            value=value,
-            parent=None,
+            ref_type=type_, is_optional=is_optional, key=name, value=value, parent=None,
         )
     return d
 
@@ -147,11 +146,7 @@ def get_dataclass_data(obj: Any) -> Dict[str, Any]:
                     )
 
         d[name] = _maybe_wrap(
-            annotated_type=type_,
-            is_optional=is_optional,
-            key=name,
-            value=value,
-            parent=None,
+            ref_type=type_, is_optional=is_optional, key=name, value=value, parent=None,
         )
     return d
 
@@ -249,12 +244,10 @@ def get_value_kind(value: Any, return_match_list: bool = False) -> Any:
     from .base import Container
 
     if isinstance(value, Container):
-        if value._is_interpolation():
-            return ret(ValueKind.MANDATORY_MISSING)
-        if value._is_missing():
-            return ret(ValueKind.MANDATORY_MISSING)
-    value = _get_value(value)
+        if value._is_interpolation() or value._is_missing():
+            value = value._value()
 
+    value = _get_value(value)
     if value == "???":
         return ret(ValueKind.MANDATORY_MISSING)
 
@@ -309,37 +302,67 @@ def is_primitive_list(obj: Any) -> bool:
 
 
 def is_primitive_dict(obj: Any) -> bool:
-    from .base import Container
+    t = get_type_of(obj)
+    return t is dict
+
+
+def is_dict_annotation(type_: Any) -> bool:
+    origin = getattr(type_, "__origin__", None)
+    return origin is Dict or origin is dict
+
+
+def is_dict_subclass(type_: Any) -> bool:
+    return type_ is not None and isinstance(type_, type) and issubclass(type_, Dict)
 
-    return not isinstance(obj, Container) and isinstance(obj, (dict))
+
+def is_dict(obj: Any) -> bool:
+    return is_primitive_dict(obj) or is_dict_annotation(obj) or is_dict_subclass(obj)
 
 
 def is_primitive_container(obj: Any) -> bool:
     return is_primitive_list(obj) or is_primitive_dict(obj)
 
 
-def _get_key_value_types(annotated_type: Any) -> Tuple[Any, Any]:
+def get_list_element_type(ref_type: Optional[Type[Any]]) -> Optional[Type[Any]]:
+    args = getattr(ref_type, "__args__", None)
+    if ref_type is not List and args is not None and args[0] is not Any:
+        element_type = args[0]
+    else:
+        element_type = None
+
+    if not (valid_value_annotation_type(element_type)):
+        raise ValidationError(f"Unsupported value type : {element_type}")
+    assert element_type is None or isinstance(element_type, type)
+    return element_type
 
-    args = getattr(annotated_type, "__args__", None)
+
+def get_dict_key_value_types(ref_type: Any) -> Tuple[Any, Any]:
+
+    args = getattr(ref_type, "__args__", None)
     if args is None:
-        bases = getattr(annotated_type, "__orig_bases__", None)
+        bases = getattr(ref_type, "__orig_bases__", None)
         if bases is not None and len(bases) > 0:
             args = getattr(bases[0], "__args__", None)
 
     key_type: Any
     element_type: Any
-    if annotated_type is Any:
-        key_type = Any
-        element_type = Any
+    if ref_type is None:
+        key_type = None
+        element_type = None
     else:
         if args is not None:
             key_type = args[0]
             element_type = args[1]
+            # None is the sentry for any type
+            if key_type is Any:
+                key_type = None
+            if element_type is Any:
+                element_type = None
         else:
-            key_type = Any
-            element_type = Any
+            key_type = None
+            element_type = None
 
-    if not _valid_value_annotation_type(element_type) and not is_structured_config(
+    if not valid_value_annotation_type(element_type) and not is_structured_config(
         element_type
     ):
         raise ValidationError(f"Unsupported value type : {element_type}")
@@ -349,19 +372,27 @@ def _get_key_value_types(annotated_type: Any) -> Tuple[Any, Any]:
     return key_type, element_type
 
 
-def _valid_value_annotation_type(type_: Any) -> bool:
-    return type_ is Any or _is_primitive_type(type_) or is_structured_config(type_)
+def valid_value_annotation_type(type_: Any) -> bool:
+    return type_ is Any or is_primitive_type(type_) or is_structured_config(type_)
 
 
 def _valid_key_annotation_type(type_: Any) -> bool:
-    return type_ is Any or issubclass(type_, str) or issubclass(type_, Enum)
+    return type_ is None or issubclass(type_, str) or issubclass(type_, Enum)
 
 
-def _is_primitive_type(type_: Any) -> bool:
+def is_primitive_type(type_: Any) -> bool:
     type_ = get_type_of(type_)
     return issubclass(type_, Enum) or type_ in (int, float, bool, str, type(None))
 
 
+def _is_primitive_type(type_: Any) -> bool:
+    warnings.warn(
+        "use omegaconf._utils.is_primitive_type", DeprecationWarning, stacklevel=2,
+    )
+
+    return is_primitive_type(type_)
+
+
 def _is_interpolation(v: Any) -> bool:
     if isinstance(v, str):
         ret = get_value_kind(v) in (
@@ -382,3 +413,74 @@ def _get_value(value: Any) -> Any:
     if isinstance(value, ValueNode):
         value = value._value()
     return value
+
+
+def format_and_raise(
+    node: Any,
+    key: Any,
+    value: Any,
+    msg: str,
+    cause: Exception,
+    type_override: Any = None,
+) -> None:
+    def type_str(t: Any) -> str:
+        if isinstance(t, type):
+            return t.__name__
+        else:
+            return str(node._metadata.object_type)
+
+    key = key
+    if key is None:
+        node = node
+    else:
+        if node is None or node._is_none() or node._is_missing():
+            node = None
+        else:
+            if isinstance(key, slice):
+                node = node
+            else:
+                child_node = node.get_node_ex(key, validate_access=False)
+                if child_node is not None:
+                    node = child_node
+                    key = None
+
+    if node is None:
+        full_key = None
+        object_type = None
+        rt = None
+    else:
+        full_key = node._get_full_key(key=key)
+
+        ref_type: Any = node._metadata.ref_type
+        if ref_type is None:
+            ref_type = Any
+        object_type = type_str(node._metadata.object_type)
+        rt = type_str(ref_type)
+        if node._metadata.optional:
+            if ref_type is Any:
+                rt = "Any"
+            else:
+                rt = f"Optional[{rt}]"
+
+    msg = string.Template(msg).substitute(
+        REF_TYPE=rt,
+        OBJECT_TYPE=object_type,
+        KEY=key,
+        FULL_KEY=full_key,
+        VALUE=value,
+        VALUE_TYPE=f"{type(value).__name__}",
+        KEY_TYPE=f"{type(key).__name__}",
+    )
+
+    s = string.Template(
+        """$MSG
+\tfull_key: $FULL_KEY
+\treference_type=$REF_TYPE
+\tobject_type=$OBJECT_TYPE
+"""
+    )
+    message = s.substitute(
+        REF_TYPE=rt, OBJECT_TYPE=object_type, MSG=msg, FULL_KEY=full_key,
+    )
+    exception_type = type(cause) if type_override is None else type_override
+    raise exception_type(f"{message}").with_traceback(sys.exc_info()[2]) from cause
diff --git a/omegaconf/base.py b/omegaconf/base.py
index abb1485..6249b51 100644
--- a/omegaconf/base.py
+++ b/omegaconf/base.py
@@ -4,28 +4,33 @@ from dataclasses import dataclass, field
 from enum import Enum
 from typing import Any, Dict, Iterator, Optional, Tuple, Type, Union
 
-from ._utils import ValueKind, _get_value, get_value_kind
+from ._utils import ValueKind, _get_value, format_and_raise, get_value_kind
 from .errors import MissingMandatoryValue, UnsupportedInterpolationType
 
 
 @dataclass
 class Metadata:
 
-    optional: bool = True
+    ref_type: Optional[Type[Any]]
 
-    key: Any = None
+    object_type: Optional[Type[Any]]
+
+    optional: bool
+
+    key: Any
 
     # Flags have 3 modes:
     #   unset : inherit from parent (None if no parent specifies)
     #   set to true: flag is true
     #   set to false: flag is false
     flags: Dict[str, bool] = field(default_factory=dict)
+    resolver_cache: Dict[str, Any] = field(default_factory=lambda: defaultdict(dict))
 
 
 @dataclass
 class ContainerMetadata(Metadata):
-    element_type: Type[Any] = None  # type:ignore
-    resolver_cache: Dict[str, Any] = field(default_factory=lambda: defaultdict(dict))
+    key_type: Any = None
+    element_type: Any = None
 
 
 class Node(ABC):
@@ -79,58 +84,22 @@ class Node(ABC):
             # noinspection PyProtectedMember
             return parent._get_flag(flag)
 
-    def _get_full_key(self, key: Union[str, Enum, int]) -> str:
-        from .listconfig import ListConfig
-        from .omegaconf import _select_one
-
-        def prepand(full_key: str, parent_type: Any, cur_type: Any, key: Any) -> str:
-            if issubclass(parent_type, ListConfig):
-                if full_key != "":
-                    if issubclass(cur_type, ListConfig):
-                        full_key = f"[{key}]{full_key}"
-                    else:
-                        full_key = f"[{key}].{full_key}"
-                else:
-                    full_key = f"[{key}]"
-            else:
-                if full_key == "":
-                    full_key = key
-                else:
-                    if issubclass(cur_type, ListConfig):
-                        full_key = f"{key}{full_key}"
-                    else:
-                        full_key = f"{key}.{full_key}"
-            return full_key
-
-        if key is not None and key != "":
-            assert isinstance(self, Container)
-            cur, _ = _select_one(c=self, key=str(key), throw_on_missing=False)
-            if cur is None:
-                cur = self
-                full_key = prepand("", type(cur), None, key)
-                if cur._key() is not None:
-                    full_key = prepand(
-                        full_key, type(cur._get_parent()), type(cur), cur._key()
-                    )
-            else:
-                full_key = prepand("", type(cur._get_parent()), type(cur), cur._key())
-        else:
-            cur = self
-            if cur._key() is None:
-                return ""
-            full_key = self._key()
-
-        assert cur is not None
-        while cur._get_parent() is not None:
-            cur = cur._get_parent()
-            assert cur is not None
-            key = cur._key()
-            if key is not None:
-                full_key = prepand(
-                    full_key, type(cur._get_parent()), type(cur), cur._key()
-                )
+    def _format_and_raise(
+        self, key: Any, value: Any, cause: Exception, type_override: Any = None,
+    ) -> None:
+        format_and_raise(
+            node=self,
+            key=key,
+            value=value,
+            msg=str(cause),
+            cause=cause,
+            type_override=type_override,
+        )
+        assert False
 
-        return full_key
+    @abstractmethod
+    def _get_full_key(self, key: Union[str, Enum, int, None]) -> str:
+        ...  # pragma: no cover
 
     def _dereference_node(self, throw_on_missing: bool = False) -> "Node":
         from .nodes import StringNode
@@ -161,13 +130,13 @@ class Node(ABC):
                     parent=parent,
                     is_optional=self._metadata.optional,
                 )
-            assert False  # pragma: no cover
+            assert False
         else:
             # not interpolation, compare directly
             if throw_on_missing:
                 value = self._value()
                 if value == "???":
-                    raise MissingMandatoryValue(self._get_full_key(""))
+                    raise MissingMandatoryValue("Missing mandatory value")
             return self
 
     @abstractmethod
@@ -232,11 +201,11 @@ class Container(Node):
     def select(self, key: str, throw_on_missing: bool = False) -> Any:
         ...  # pragma: no cover
 
-    def get_node(self, key: Any) -> Node:
+    def get_node(self, key: Any) -> Optional[Node]:
         ...  # pragma: no cover
 
     @abstractmethod
-    def __delitem__(self, key: Union[str, int, slice]) -> None:
+    def __delitem__(self, key: Any) -> None:
         ...  # pragma: no cover
 
     @abstractmethod
@@ -313,25 +282,28 @@ class Container(Node):
 
             if parent is None or (value is None and last_key not in parent):  # type: ignore
                 raise KeyError(
-                    "{} interpolation key '{}' not found".format(inter_type, inter_key)
+                    f"{inter_type} interpolation key '{inter_key}' not found"
                 )
             assert isinstance(value, Node)
             return value
         else:
             resolver = OmegaConf.get_resolver(inter_type)
             if resolver is not None:
-                value = resolver(root_node, inter_key)
-
-                node = self.get_node(key)
-                return ValueNode(
-                    value=value,
-                    parent=self,
-                    key=key,
-                    is_optional=node._metadata.optional,
-                )
+                try:
+                    value = resolver(root_node, inter_key)
+                    return ValueNode(
+                        value=value,
+                        parent=self,
+                        metadata=Metadata(
+                            ref_type=None, object_type=None, key=key, optional=True
+                        ),
+                    )
+                except Exception as e:
+                    self._format_and_raise(key=inter_key, value=None, cause=e)
+                    assert False
             else:
                 raise UnsupportedInterpolationType(
-                    "Unsupported interpolation type {}".format(inter_type)
+                    f"Unsupported interpolation type {inter_type}"
                 )
 
     def _resolve_str_interpolation(
@@ -371,4 +343,4 @@ class Container(Node):
             new += orig[last_index:]
             return StringNode(value=new, key=key)
         else:
-            assert False  # pragma: no cover
+            assert False
diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py
index db52399..85be363 100644
--- a/omegaconf/basecontainer.py
+++ b/omegaconf/basecontainer.py
@@ -13,13 +13,16 @@ from ._utils import (
     _is_interpolation,
     get_value_kind,
     get_yaml_loader,
+    is_dict_annotation,
+    is_dict_subclass,
     is_primitive_container,
+    is_primitive_dict,
     is_structured_config,
 )
 from .base import Container, ContainerMetadata, Node
-from .errors import MissingMandatoryValue, ReadonlyConfigError, ValidationError
+from .errors import MissingMandatoryValue, ReadonlyConfigError
 
-DEFAULT_VALUE_MARKER: Any = object()
+DEFAULT_VALUE_MARKER: Any = str("__DEFAULT_VALUE_MARKER__")
 
 
 class BaseContainer(Container, ABC):
@@ -61,15 +64,14 @@ class BaseContainer(Container, ABC):
         ):
             value = default_value
 
-        value = self._resolve_str_interpolation(
+        resolved = self._resolve_str_interpolation(
             key=key, value=value, throw_on_missing=True
         )
-        if is_mandatory_missing(value):
-            raise MissingMandatoryValue(self._get_full_key(str(key)))
+        if is_mandatory_missing(resolved):
+            raise MissingMandatoryValue("Missing mandatory value: $FULL_KEY")
+        resolved2 = _get_value(resolved)
 
-        value = _get_value(value)
-
-        return value
+        return resolved2
 
     def __str__(self) -> str:
         return self.__repr__()
@@ -91,10 +93,9 @@ class BaseContainer(Container, ABC):
     def __setstate__(self, d: Dict[str, Any]) -> None:
         self.__dict__.update(d)
 
-    def __delitem__(self, key: Union[str, int, slice]) -> None:
-        if self._get_flag("readonly"):
-            raise ReadonlyConfigError(self._get_full_key(str(key)))
-        del self.__dict__["_content"][key]
+    @abstractmethod
+    def __delitem__(self, key: Any) -> None:
+        ...  # pragma: no cover
 
     def __len__(self) -> int:
         return self.__dict__["_content"].__len__()  # type: ignore
@@ -154,14 +155,19 @@ class BaseContainer(Container, ABC):
             root[idx] = value
 
     def select(self, key: str, throw_on_missing: bool = False) -> Any:
-        _root, _last_key, value = self._select_impl(key, throw_on_missing=False)
-        if value is not None and value._is_missing():
-            if throw_on_missing:
-                raise MissingMandatoryValue(value._get_full_key(""))
-            else:
-                return None
+        try:
+            _root, _last_key, value = self._select_impl(key, throw_on_missing=False)
+            if value is not None and value._is_missing():
+                if throw_on_missing:
+                    raise MissingMandatoryValue(
+                        f"Missing mandatory value : {self._get_full_key('')}"
+                    )
+                else:
+                    return None
 
-        return _get_value(value)
+            return _get_value(value)
+        except Exception as e:
+            self._format_and_raise(key=key, value=None, cause=e)
 
     def is_empty(self) -> bool:
         """return true if config is empty"""
@@ -205,7 +211,7 @@ class BaseContainer(Container, ABC):
                 retlist.append(item)
             return retlist
 
-        assert False  # pragma: no cover
+        assert False
 
     def to_container(self, resolve: bool = False) -> Union[Dict[str, Any], List[Any]]:
         warnings.warn(
@@ -217,8 +223,6 @@ class BaseContainer(Container, ABC):
         return BaseContainer._to_content(self, resolve)
 
     def pretty(self, resolve: bool = False, sort_keys: bool = False) -> str:
-        from omegaconf import OmegaConf
-
         """
         returns a yaml dump of this config object.
         :param resolve: if True, will return a string with the interpolations resolved, otherwise
@@ -226,6 +230,8 @@ class BaseContainer(Container, ABC):
         :param sort_keys: If True, will print dict keys in sorted order. default False.
         :return: A string containing the yaml representation.
         """
+        from omegaconf import OmegaConf
+
         container = OmegaConf.to_container(self, resolve=resolve, enum_to_str=True)
         return yaml.dump(  # type: ignore
             container, default_flow_style=False, allow_unicode=True, sort_keys=sort_keys
@@ -243,45 +249,46 @@ class BaseContainer(Container, ABC):
         assert isinstance(src, DictConfig)
         src = copy.deepcopy(src)
         src_type = src._metadata.object_type
-        dest_type = dest._metadata.object_type
-
-        if src_type is not None and src_type is not dest_type:
-            prototype = DictConfig(annotated_type=src_type, content=src_type,)
-
-            dest.__dict__["_content"] = copy.deepcopy(prototype.__dict__["_content"])
-            dest.__dict__["_metadata"] = copy.deepcopy(prototype._metadata)
 
-        for key, value in src.items_ex(resolve=False):
+        # disable object time during the merge
+        type_backup = dest._metadata.object_type
+        dest._metadata.object_type = None
 
+        dest._validate_set_merge_impl(key=None, value=src, is_assign=False)
+        for key, src_value in src.items_ex(resolve=False):
             dest_element_type = dest._metadata.element_type
-            typed = dest_element_type not in (None, Any)
+            element_typed = dest_element_type not in (None, Any)
             if OmegaConf.is_missing(dest, key):
-                if isinstance(value, DictConfig):
-                    if OmegaConf.is_missing(src, key):
-                        dest[key] = DictConfig(content="???")
-                    else:
-                        dest[key] = {}
-            if (dest.get_node(key) is not None) or typed:
+                if isinstance(src_value, DictConfig):
+                    if OmegaConf.is_missing(dest, key):
+                        dest[key] = src_value
+
+            if (dest.get_node(key) is not None) or element_typed:
                 dest_node = dest.get_node(key)
-                if dest_node is None and typed:
+                if dest_node is None and element_typed:
                     dest[key] = DictConfig(content=dest_element_type, parent=dest)
                     dest_node = dest.get_node(key)
 
                 if isinstance(dest_node, BaseContainer):
-                    if isinstance(value, BaseContainer):
-                        dest._validate_set(key=key, value=value)
-                        dest_node.merge_with(value)
+                    if isinstance(src_value, BaseContainer):
+                        dest._validate_merge(key=key, value=src_value)
+                        dest_node.merge_with(src_value)
                     else:
-                        dest.__setitem__(key, value)
+                        dest.__setitem__(key, src_value)
                 else:
-                    if isinstance(value, BaseContainer):
-                        dest.__setitem__(key, value)
+                    if isinstance(src_value, BaseContainer):
+                        dest.__setitem__(key, src_value)
                     else:
                         assert isinstance(dest_node, ValueNode)
-                        dest_node._set_value(value)
+                        dest_node._set_value(src_value)
             else:
                 dest[key] = src.get_node(key)
 
+        if src_type is not Any and not is_primitive_dict(src_type):
+            dest._metadata.object_type = src_type
+        else:
+            dest._metadata.object_type = type_backup
+
     def merge_with(
         self,
         *others: Union["BaseContainer", Dict[str, Any], List[Any], Tuple[Any], Any],
@@ -306,7 +313,7 @@ class BaseContainer(Container, ABC):
                 for item in other:
                     self.append(item)
             else:
-                raise TypeError("Merging DictConfig with ListConfig is not supported")
+                raise TypeError("Cannot merge DictConfig with ListConfig")
 
         # recursively correct the parent hierarchy after the merge
         self._re_parent()
@@ -317,10 +324,16 @@ class BaseContainer(Container, ABC):
 
         from .nodes import ValueNode
 
-        self._validate_get(key)
-        self._validate_set(key, value)
+        if isinstance(value, Node):
+            try:
+                old = value._key()
+                value._set_key(key)
+                self._validate_set(key, value)
+            finally:
+                value._set_key(old)
+        else:
+            self._validate_set(key, value)
 
-        must_wrap = is_primitive_container(value)
         input_config = isinstance(value, Container)
         target_node_ref = self.get_node(key)
         special_value = value is None or value == "???"
@@ -340,12 +353,23 @@ class BaseContainer(Container, ABC):
             target_node = isinstance(target_node_ref, ValueNode)
 
         def wrap(key: Any, val: Any) -> Node:
-            if is_structured_config(val):
-                type_ = OmegaConf.get_type(val)
+            if is_dict_annotation(self._metadata.ref_type) or (
+                not is_primitive_dict(self._metadata.ref_type)
+                and is_dict_subclass(self._metadata.ref_type)
+            ):
+                ref_type = self._metadata.element_type
             else:
-                type_ = self._metadata.element_type
+                target = self.get_node(key)
+                if target is None:
+                    if is_structured_config(val):
+                        ref_type = OmegaConf.get_type(val)
+                    else:
+                        # assignment
+                        ref_type = OmegaConf.get_ref_type(val)
+                else:
+                    ref_type = OmegaConf.get_ref_type(target)
             return _maybe_wrap(
-                annotated_type=type_, key=key, value=val, is_optional=True, parent=self,
+                ref_type=ref_type, key=key, value=val, is_optional=True, parent=self
             )
 
         def assign(value_key: Any, value_to_assign: Any) -> None:
@@ -353,34 +377,27 @@ class BaseContainer(Container, ABC):
             value_to_assign._set_key(value_key)
             self.__dict__["_content"][value_key] = value_to_assign
 
-        try:
-            if must_wrap:
-                self.__dict__["_content"][key] = wrap(key, value)
-            elif input_node and target_node:
-                # both nodes, replace existing node with new one
+        if is_primitive_container(value):
+            self.__dict__["_content"][key] = wrap(key, value)
+        elif input_node and target_node:
+            # both nodes, replace existing node with new one
+            assign(key, value)
+        elif not input_node and target_node:
+            # input is not node, can be primitive or config
+            if input_config:
                 assign(key, value)
-            elif not input_node and target_node:
-                # input is not node, can be primitive or config
-                if input_config:
-                    assign(key, value)
-                else:
-                    self.__dict__["_content"][key]._set_value(value)
-            elif input_node and not target_node:
-                # target must be config, replace target with input node
+            else:
+                self.__dict__["_content"][key]._set_value(value)
+        elif input_node and not target_node:
+            # target must be config, replace target with input node
+            assign(key, value)
+        elif not input_node and not target_node:
+            if should_set_value:
+                self.__dict__["_content"][key]._set_value(value)
+            elif input_config:
                 assign(key, value)
-            elif not input_node and not target_node:
-                if should_set_value:
-                    self.__dict__["_content"][key]._set_value(value)
-                elif input_config:
-                    assign(key, value)
-                else:
-                    self.__dict__["_content"][key] = wrap(key, value)
-        except ValidationError as ve:
-            import sys
-
-            raise type(ve)(
-                f"Error setting '{self._get_full_key(str(key))} = {value}' : {ve}"
-            ).with_traceback(sys.exc_info()[2]) from None
+            else:
+                self.__dict__["_content"][key] = wrap(key, value)
 
     @staticmethod
     def _item_eq(
@@ -472,7 +489,7 @@ class BaseContainer(Container, ABC):
         return _is_interpolation(self.__dict__["_content"])
 
     @abstractmethod
-    def _validate_get(self, key: Any) -> None:
+    def _validate_get(self, key: Any, value: Any = None) -> None:
         ...  # pragma: no cover
 
     @abstractmethod
@@ -481,3 +498,58 @@ class BaseContainer(Container, ABC):
 
     def _value(self) -> Any:
         return self.__dict__["_content"]
+
+    def _get_full_key(self, key: Union[str, Enum, int, None]) -> str:
+        from .listconfig import ListConfig
+        from .omegaconf import _select_one
+
+        def prepand(full_key: str, parent_type: Any, cur_type: Any, key: Any) -> str:
+            if issubclass(parent_type, ListConfig):
+                if full_key != "":
+                    if issubclass(cur_type, ListConfig):
+                        full_key = f"[{key}]{full_key}"
+                    else:
+                        full_key = f"[{key}].{full_key}"
+                else:
+                    full_key = f"[{key}]"
+            else:
+                if full_key == "":
+                    full_key = key
+                else:
+                    if issubclass(cur_type, ListConfig):
+                        full_key = f"{key}{full_key}"
+                    else:
+                        full_key = f"{key}.{full_key}"
+            return full_key
+
+        if key is not None and key != "":
+            assert isinstance(self, Container)
+            cur, _ = _select_one(
+                c=self, key=str(key), throw_on_missing=False, throw_on_type_error=False,
+            )
+            if cur is None:
+                cur = self
+                full_key = prepand("", type(cur), None, key)
+                if cur._key() is not None:
+                    full_key = prepand(
+                        full_key, type(cur._get_parent()), type(cur), cur._key()
+                    )
+            else:
+                full_key = prepand("", type(cur._get_parent()), type(cur), cur._key())
+        else:
+            cur = self
+            if cur._key() is None:
+                return ""
+            full_key = self._key()
+
+        assert cur is not None
+        while cur._get_parent() is not None:
+            cur = cur._get_parent()
+            assert cur is not None
+            key = cur._key()
+            if key is not None:
+                full_key = prepand(
+                    full_key, type(cur._get_parent()), type(cur), cur._key()
+                )
+
+        return full_key
diff --git a/omegaconf/dictconfig.py b/omegaconf/dictconfig.py
index 0143758..c14df8b 100644
--- a/omegaconf/dictconfig.py
+++ b/omegaconf/dictconfig.py
@@ -1,5 +1,4 @@
 import copy
-from dataclasses import dataclass
 from enum import Enum
 from typing import (
     AbstractSet,
@@ -16,9 +15,13 @@ from typing import (
 )
 
 from ._utils import (
+    ValueKind,
     _is_interpolation,
+    get_dict_key_value_types,
     get_structured_config_data,
     get_type_of,
+    get_value_kind,
+    is_dict,
     is_primitive_dict,
     is_structured_config,
     is_structured_config_frozen,
@@ -30,50 +33,40 @@ from .errors import (
     MissingMandatoryValue,
     ReadonlyConfigError,
     UnsupportedInterpolationType,
-    UnsupportedValueType,
     ValidationError,
 )
 from .nodes import EnumNode, ValueNode
 
 
-@dataclass
-class DictConfigMetadata(ContainerMetadata):
-    object_type: Optional[Type[Any]] = None
-    annotated_type: Optional[Type[Any]] = None
-    key_type: Any = Any
-
-
 class DictConfig(BaseContainer, MutableMapping[str, Any]):
 
-    _metadata: DictConfigMetadata
+    _metadata: ContainerMetadata
 
     def __init__(
         self,
         content: Union[Dict[str, Any], Any],
         key: Any = None,
         parent: Optional[Container] = None,
-        annotated_type: Optional[Type[Any]] = None,
+        ref_type: Optional[Type[Any]] = None,
         is_optional: bool = True,
-        key_type: Any = Any,
-        element_type: Any = Any,
     ) -> None:
+        key_type, element_type = get_dict_key_value_types(ref_type)
         super().__init__(
             parent=parent,
-            metadata=DictConfigMetadata(
+            metadata=ContainerMetadata(
                 key=key,
                 optional=is_optional,
-                element_type=element_type,
+                ref_type=ref_type,
                 object_type=None,
                 key_type=key_type,
+                element_type=element_type,
             ),
         )
 
-        if is_structured_config(annotated_type):
-            self._metadata.annotated_type = annotated_type
-        if is_structured_config(content) or is_structured_config(annotated_type):
+        if is_structured_config(content) or is_structured_config(ref_type):
             self._set_value(content)
             if is_structured_config_frozen(content) or is_structured_config_frozen(
-                annotated_type
+                ref_type
             ):
                 self._set_flag("readonly", True)
 
@@ -84,7 +77,6 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
                 metadata.key = key
                 metadata.optional = is_optional
                 metadata.element_type = element_type
-                metadata.object_type = None
                 metadata.key_type = key_type
                 self.__dict__["_metadata"] = metadata
 
@@ -96,7 +88,7 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         return res
 
     def __copy__(self) -> "DictConfig":
-        res = DictConfig(content={}, element_type=self._metadata.element_type)
+        res = DictConfig(content=None)
         for k, v in self.__dict__.items():
             res.__dict__[k] = copy.copy(v)
         res._re_parent()
@@ -105,122 +97,163 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
     def copy(self) -> "DictConfig":
         return copy.copy(self)
 
-    def _validate_get(self, key: Union[int, str, Enum]) -> None:
-        is_typed = self._metadata.object_type is not None
+    def _validate_get(self, key: Any, value: Any = None) -> None:
+        is_typed = self._metadata.object_type not in (Any, None,) and not is_dict(
+            self._metadata.object_type
+        )
+
         is_struct = self._get_flag("struct") is True
         if key not in self.__dict__["_content"]:
             if is_typed:
                 # do not raise an exception if struct is explicitly set to False
                 if self._get_node_flag("struct") is False:
                     return
-                # Or if type is a subclass if dict
-                assert self._metadata.object_type is not None
-                if issubclass(self._metadata.object_type, dict):
-                    return
             if is_typed or is_struct:
                 if is_typed:
                     assert self._metadata.object_type is not None
-                    msg = f"Accessing unknown key in {self._metadata.object_type.__name__} : {self._get_full_key(key)}"
+                    msg = f"Key '{key}' not in '{self._metadata.object_type.__name__}'"
                 else:
-                    msg = "Accessing unknown key in a struct : {}".format(
-                        self._get_full_key(key)
-                    )
+                    msg = f"Key '{key}' in not in struct"
                 raise AttributeError(msg)
 
+    def _validate_merge(self, key: Any, value: Any) -> None:
+        self._validate_set_merge_impl(key, value, is_assign=False)
+
     def _validate_set(self, key: Any, value: Any) -> None:
-        target = self.get_node(key)
+        self._validate_set_merge_impl(key, value, is_assign=True)
+
+    def _validate_set_merge_impl(self, key: Any, value: Any, is_assign: bool) -> None:
+        from omegaconf import OmegaConf
+
+        vk = get_value_kind(value)
+        if vk == ValueKind.INTERPOLATION:
+            return
+        if isinstance(value, (str, ValueNode)) and vk == ValueKind.STR_INTERPOLATION:
+            return
+
+        if OmegaConf.is_none(value):
+            if key is not None:
+                node = self.get_node(key)
+                if node is not None and not node._is_optional():
+                    raise ValidationError("field '$FULL_KEY' is not Optional")
+            else:
+                if not self._is_optional():
+                    raise ValidationError("field '$FULL_KEY' is not Optional")
 
         if value == "???":
             return
 
-        if target is not None:
-            if target._get_flag("readonly"):
-                raise ReadonlyConfigError(self._get_full_key(key))
+        # validate get
+        if key is not None:
+            try:
+                self._validate_get(key, value)
+            except AttributeError as e:
+                raise AttributeError(f"Error setting $KEY=$VALUE : {e}")
+
+        target: Optional[Node]
+        if key is None:
+            target = self
         else:
-            if self._get_flag("readonly"):
-                raise ReadonlyConfigError(self._get_full_key(key))
+            target = self.get_node(key)
+
+        if (target is not None and target._get_flag("readonly")) or self._get_flag(
+            "readonly"
+        ):
+            if is_assign:
+                msg = f"Cannot assign to read-only node : {value}"
+            else:
+                msg = f"Cannot merge into read-only node : {value}"
+            raise ReadonlyConfigError(msg)
 
         if target is None:
             return
 
         def is_typed(c: Any) -> bool:
-            return isinstance(c, DictConfig) and c._metadata.object_type is not None
-
-        def get_type(c: Any) -> Optional[Type[Any]]:
-            if is_structured_config(c):
-                return get_type_of(c)
-            if isinstance(c, DictConfig):
-                t = c._metadata.object_type
-                assert t is None or isinstance(t, type)
-                return t
-            else:
-                return type(c)
+            return isinstance(c, DictConfig) and c._metadata.ref_type not in (Any, dict)
 
         if not is_typed(target):
             return
-        target_type = get_type(target)
-        value_type = get_type(value)
 
         # target must be optional by now. no need to check the type of value if None.
         if value is None:
             return
 
-        if (
-            target_type is not None
-            and value_type is not None
-            and not issubclass(value_type, target_type)
-        ):
-            raise ValidationError(
-                f"Invalid type assigned : {value_type.__name__} "
-                f"is not a subclass of {target_type.__name__}. value: {value}"
+        target_type = OmegaConf.get_ref_type(target)
+        value_type = OmegaConf.get_type(value)
+
+        if is_dict(value_type) and is_dict(target_type):
+            return
+
+        if is_assign:
+            # assign
+            validation_error = (
+                target_type is not None
+                and value_type is not None
+                and not issubclass(value_type, target_type)
             )
+        else:
+            # merge
+            validation_error = not is_dict(value_type) and (
+                target_type is not None
+                and value_type is not None
+                and not issubclass(value_type, target_type)
+            )
+
+        if validation_error:
+            assert value_type is not None
+            assert target_type is not None
+            msg = (
+                f"Invalid type assigned : {value_type.__name__} is not a "
+                f"subclass of {target_type.__name__}. value: {value}"
+            )
+            raise ValidationError(msg)
 
     def _validate_and_normalize_key(self, key: Any) -> Union[str, Enum]:
         return self._s_validate_and_normalize_key(self._metadata.key_type, key)
 
-    @staticmethod
-    def _s_validate_and_normalize_key(key_type: Any, key: Any) -> Union[str, Enum]:
-        if key_type is Any:
+    def _s_validate_and_normalize_key(
+        self, key_type: Any, key: Any
+    ) -> Union[str, Enum]:
+        if key_type is None:
             for t in (str, Enum):
                 try:
-                    return DictConfig._s_validate_and_normalize_key(key_type=t, key=key)
+                    return self._s_validate_and_normalize_key(key_type=t, key=key)
                 except KeyValidationError:
                     pass
-            raise KeyValidationError(
-                f"Key {key} (type {type(key).__name__}) is incompatible with {key_type}"
-            )
-
-        if key_type == str:
-            if not isinstance(key, str):
-                raise KeyValidationError(
-                    f"Key {key} is incompatible with {key_type.__name__}"
-                )
-            return key
-
-        try:
-            ret = EnumNode.validate_and_convert_to_enum(key_type, key)
-            assert ret is not None
-            return ret
-        except ValidationError as e:
-            raise KeyValidationError(
-                f"Key {key} is incompatible with {key_type.__name__} : {e}"
-            )
+            raise KeyValidationError("Incompatible key type '$KEY_TYPE'")
+        else:
+            if key_type == str:
+                if not isinstance(key, str):
+                    raise KeyValidationError(
+                        f"Key $KEY ($KEY_TYPE) is incompatible with ({key_type.__name__})"
+                    )
+                return key
+            elif issubclass(key_type, Enum):
+                try:
+                    ret = EnumNode.validate_and_convert_to_enum(key_type, key)
+                    assert ret is not None
+                    return ret
+                except ValidationError as e:
+                    raise KeyValidationError(
+                        f"Key '$KEY' is incompatible with ({key_type.__name__}) : {e}"
+                    )
+            else:
+                assert False
 
     def __setitem__(self, key: Union[str, Enum], value: Any) -> None:
+
         try:
-            self.__set_impl(key, value)
+            self.__set_impl(key=key, value=value)
         except AttributeError as e:
-            raise KeyError(str(e))
+            self._format_and_raise(
+                key=key, value=value, type_override=KeyError, cause=e
+            )
+        except Exception as e:
+            self._format_and_raise(key=key, value=value, cause=e)
 
     def __set_impl(self, key: Union[str, Enum], value: Any) -> None:
         key = self._validate_and_normalize_key(key)
-
-        try:
-            self._set_item_impl(key, value)
-        except UnsupportedValueType as ex:
-            raise UnsupportedValueType(
-                f"'{type(value).__name__}' is not a supported type (key: {self._get_full_key(key)}) : {ex}"
-            )
+        self._set_item_impl(key, value)
 
     # hide content while inspecting in debugger
     def __dir__(self) -> Iterable[str]:
@@ -235,7 +268,11 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         :param value:
         :return:
         """
-        self.__set_impl(key, value)
+        try:
+            self.__set_impl(key, value)
+        except Exception as e:
+            self._format_and_raise(key=key, value=value, cause=e)
+            assert False
 
     def __getattr__(self, key: str) -> Any:
         """
@@ -247,7 +284,13 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         if key == "__members__":
             raise AttributeError()
 
-        return self.get(key=key)
+        if key == "__name__":
+            raise AttributeError()
+
+        try:
+            return self._get_impl(key=key, default_value=DEFAULT_VALUE_MARKER)
+        except Exception as e:
+            self._format_and_raise(key=key, value=None, cause=e)
 
     def __getitem__(self, key: Union[str, Enum]) -> Any:
         """
@@ -255,21 +298,41 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         :param key:
         :return:
         """
+
         try:
-            return self.get(key=key)
+            return self._get_impl(key=key, default_value=DEFAULT_VALUE_MARKER)
         except AttributeError as e:
-            raise KeyError(str(e))
+            raise KeyError(f"Error getting '{key}' : {e}")
+        except Exception as e:
+            self._format_and_raise(key=key, value=None, cause=e)
+
+    def __delitem__(self, key: Union[str, int, Enum]) -> None:
+        if self._get_flag("readonly"):
+            self._format_and_raise(
+                key=key,
+                value=None,
+                cause=ReadonlyConfigError(
+                    "Cannot delete item from read-only DictConfig"
+                ),
+            )
+        del self.__dict__["_content"][key]
 
     def get(
         self, key: Union[str, Enum], default_value: Any = DEFAULT_VALUE_MARKER,
     ) -> Any:
+        try:
+            return self._get_impl(key=key, default_value=default_value)
+        except Exception as e:
+            self._format_and_raise(key=key, value=None, cause=e)
+
+    def _get_impl(self, key: Union[str, Enum], default_value: Any) -> Any:
         key = self._validate_and_normalize_key(key)
         node = self.get_node_ex(key=key, default_value=default_value)
         return self._resolve_with_default(
             key=key, value=node, default_value=default_value,
         )
 
-    def get_node(self, key: Union[str, Enum]) -> Node:
+    def get_node(self, key: Union[str, Enum]) -> Optional[Node]:
         return self.get_node_ex(key, default_value=DEFAULT_VALUE_MARKER)
 
     def get_node_ex(
@@ -277,7 +340,7 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         key: Union[str, Enum],
         default_value: Any = DEFAULT_VALUE_MARKER,
         validate_access: bool = True,
-    ) -> Node:
+    ) -> Optional[Node]:
         value: Node = self.__dict__["_content"].get(key)
         if validate_access:
             try:
@@ -287,26 +350,44 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
                     value = default_value
                 else:
                     raise
-        else:
-            if default_value is not DEFAULT_VALUE_MARKER:
-                value = default_value
-        return value
 
-    __pop_marker = object()
+        if default_value is not DEFAULT_VALUE_MARKER:
+            value = default_value
 
-    def pop(self, key: Union[str, Enum], default: Any = __pop_marker) -> Any:
-        key = self._validate_and_normalize_key(key)
-        if self._get_flag("readonly"):
-            raise ReadonlyConfigError(self._get_full_key(key))
-        value = self._resolve_with_default(
-            key=key,
-            value=self.__dict__["_content"].pop(key, default),
-            default_value=default,
-        )
-        if value is self.__pop_marker:
-            raise KeyError(key)
         return value
 
+    def pop(self, key: Union[str, Enum], default: Any = DEFAULT_VALUE_MARKER) -> Any:
+        try:
+            key = self._validate_and_normalize_key(key)
+            if self._get_flag("readonly"):
+                self._format_and_raise(
+                    key=key,
+                    value=None,
+                    cause=ReadonlyConfigError("Cannot pop from read-only node"),
+                )
+
+            node = self.get_node(key=key)
+            if node is not None:
+                value = self._resolve_with_default(
+                    key=key, value=node, default_value=default
+                )
+                del self[key]
+                return value
+            else:
+                if default is not DEFAULT_VALUE_MARKER:
+                    return default
+                else:
+                    full = self._get_full_key(key)
+                    if full != key:
+                        raise KeyError(f"Key not found: '{key}' (path: '{full}')")
+                    else:
+                        raise KeyError(f"Key not found: '{key}'")
+        except Exception as e:
+            if isinstance(e, KeyError):
+                raise
+            else:
+                self._format_and_raise(key=key, value=None, cause=e)
+
     def keys(self) -> Any:
         if self._is_missing() or self._is_interpolation() or self._is_none():
             return list()
@@ -389,55 +470,53 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         if type_or_prototype is None:
             return
         if not is_structured_config(type_or_prototype):
-            raise ValueError("Expected structured config class")
+            raise ValueError(f"Expected structured config class : {type_or_prototype}")
 
         from omegaconf import OmegaConf
 
         proto: DictConfig = OmegaConf.structured(type_or_prototype)
-        type_ = proto._metadata.object_type
+        object_type = proto._metadata.object_type
         # remove the type to prevent assignment validation from rejecting the promotion.
         proto._metadata.object_type = None
         self.merge_with(proto)
         # restore the type.
-        self._metadata.object_type = type_
+        self._metadata.object_type = object_type
 
     def _set_value(self, value: Any) -> None:
         from omegaconf import OmegaConf
 
-        self._metadata.object_type = self._metadata.annotated_type
-        type_ = (
-            self._metadata.object_type
-            if self._metadata.object_type is not None
-            else DictConfig
-        )
+        assert not isinstance(value, ValueNode)
+        self._validate_set(key=None, value=value)
+
         if OmegaConf.is_none(value):
-            if not self._is_optional():
-                assert isinstance(type_, type)
-                raise ValidationError(
-                    f"Cannot assign {type_.__name__}=None (field is not Optional)"
-                )
             self.__dict__["_content"] = None
+            self._metadata.object_type = None
         elif _is_interpolation(value):
             self.__dict__["_content"] = value
-        elif value == "???":  # missing
+            self._metadata.object_type = None
+        elif value == "???":
             self.__dict__["_content"] = "???"
-        else:
-            is_structured = is_structured_config(value)
-            if is_structured:
-                _type = get_type_of(value)
-                value = get_structured_config_data(value)
-
             self._metadata.object_type = None
+        else:
             self.__dict__["_content"] = {}
-            if isinstance(value, DictConfig):
+            if is_structured_config(value):
+                self._metadata.object_type = None
+                data = get_structured_config_data(value)
+                for k, v in data.items():
+                    self.__setitem__(k, v)
+                self._metadata.object_type = get_type_of(value)
+            elif isinstance(value, DictConfig):
+                self._metadata.object_type = dict
                 for k, v in value.items_ex(resolve=False):
                     self.__setitem__(k, v)
-            else:
+                self.__dict__["_metadata"] = copy.deepcopy(value._metadata)
+
+            elif isinstance(value, dict):
+                self._metadata.object_type = self._metadata.ref_type
                 for k, v in value.items():
                     self.__setitem__(k, v)
-
-            if is_structured:
-                self._metadata.object_type = _type
+            else:
+                assert False, f"Unsupported value type : {value}"  # pragma: no cover
 
     @staticmethod
     def _dict_conf_eq(d1: "DictConfig", d2: "DictConfig") -> bool:
diff --git a/omegaconf/listconfig.py b/omegaconf/listconfig.py
index 688d8f0..6426e31 100644
--- a/omegaconf/listconfig.py
+++ b/omegaconf/listconfig.py
@@ -10,52 +10,66 @@ from typing import (
     MutableSequence,
     Optional,
     Tuple,
+    Type,
     Union,
 )
 
-from ._utils import ValueKind, get_value_kind, is_primitive_list, isint
+from ._utils import ValueKind, get_list_element_type, get_value_kind, is_primitive_list
 from .base import Container, ContainerMetadata, Node
 from .basecontainer import BaseContainer
 from .errors import (
     KeyValidationError,
+    MissingMandatoryValue,
     ReadonlyConfigError,
-    UnsupportedValueType,
     ValidationError,
 )
-from .nodes import AnyNode, ValueNode
+from .nodes import ValueNode
 
 
 class ListConfig(BaseContainer, MutableSequence[Any]):
+
+    _content: Union[List[Optional[Node]], None, str]
+
     def __init__(
         self,
         content: Union[List[Any], Tuple[Any, ...], str, None],
         key: Any = None,
         parent: Optional[Container] = None,
+        ref_type: Optional[Type[Any]] = None,
         is_optional: bool = True,
-        element_type: Any = Any,
     ) -> None:
+        element_type = get_list_element_type(ref_type)
         super().__init__(
             parent=parent,
             metadata=ContainerMetadata(
-                key=key, optional=is_optional, element_type=element_type
+                ref_type=ref_type,
+                object_type=list,
+                key=key,
+                optional=is_optional,
+                element_type=element_type,
+                key_type=int,
             ),
         )
+        self._content = None
         self._set_value(value=content)
 
-    def _validate_get(self, index: Any) -> None:
-        if not isinstance(index, (int, slice)):
-            raise KeyValidationError(f"Key type {type(index).__name__} is invalid")
+    def _validate_get(self, key: Any, value: Any = None) -> None:
+        if not isinstance(key, (int, slice)):
+            raise KeyValidationError("Invalid key type '$KEY_TYPE'")
 
     def _validate_set(self, key: Any, value: Any) -> None:
+
+        self._validate_get(key, value)
+
         if self._get_flag("readonly"):
-            raise ReadonlyConfigError(self._get_full_key(f"{key}"))
+            raise ReadonlyConfigError("ListConfig is read-only")
 
         if 0 <= key < self.__len__():
             target = self.get_node(key)
-            if isinstance(target, Container):
+            if target is not None:
                 if value is None and not target._is_optional():
                     raise ValidationError(
-                        "Non optional ListConfig node cannot be assigned None"
+                        "$FULL_KEY is not optional and cannot be assigned None"
                     )
 
     def __deepcopy__(self, memo: Dict[int, Any] = {}) -> "ListConfig":
@@ -65,84 +79,112 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
         res._re_parent()
         return res
 
-    def __getattr__(self, key: str) -> Any:
-        if isinstance(key, str) and isint(key):
-            return self.__getitem__(int(key))
-        else:
-            raise AttributeError()
-
     # hide content while inspecting in debugger
     def __dir__(self) -> Iterable[str]:
         return [str(x) for x in range(0, len(self))]
 
     def __len__(self) -> int:
+        if self._is_none():
+            return 0
         if self._is_missing():
             return 0
+        assert isinstance(self._content, list)
         return len(self._content)
 
     def __getitem__(self, index: Union[int, slice]) -> Any:
-        assert isinstance(index, (int, slice))
-        self._validate_get(index)
-
-        if isinstance(index, slice):
-            result = []
-            for slice_idx in itertools.islice(
-                range(0, len(self)), index.start, index.stop, index.step
-            ):
-                val = self._resolve_with_default(
-                    key=slice_idx, value=self._content[slice_idx], default_value=None
+        try:
+            if self._is_missing():
+                raise MissingMandatoryValue("ListConfig is missing")
+            self._validate_get(index, None)
+            if self._is_none():
+                raise TypeError(
+                    "ListConfig object representing None is not subscriptable"
                 )
-                result.append(val)
-            return result
-        else:
-            return self._resolve_with_default(
-                key=index, value=self._content[index], default_value=None
-            )
 
-    def _set_at_index(self, index: Union[int, slice], value: Any) -> None:
-        try:
-            self._set_item_impl(index, value)
-        except UnsupportedValueType:
-            full_key = self._get_full_key(str(index))
+            assert isinstance(self._content, list)
+            if isinstance(index, slice):
+                result = []
+                for slice_idx in itertools.islice(
+                    range(0, len(self)), index.start, index.stop, index.step
+                ):
+                    val = self._resolve_with_default(
+                        key=slice_idx, value=self._content[slice_idx]
+                    )
+                    result.append(val)
+                return result
+            else:
+                return self._resolve_with_default(key=index, value=self._content[index])
+        except Exception as e:
+            self._format_and_raise(key=index, value=None, cause=e)
 
-            raise UnsupportedValueType(
-                f"{type(value).__name__} is not a supported type (key: {full_key})"
-            )
+    def _set_at_index(self, index: Union[int, slice], value: Any) -> None:
+        self._set_item_impl(index, value)
 
     def __setitem__(self, index: Union[int, slice], value: Any) -> None:
-        self._set_at_index(index, value)
+        try:
+            self._set_at_index(index, value)
+        except Exception as e:
+            self._format_and_raise(key=index, value=value, cause=e)
 
     def append(self, item: Any) -> None:
-        index = len(self)
-        self._validate_set(key=index, value=item)
-
         try:
             from omegaconf.omegaconf import OmegaConf, _maybe_wrap
 
-            self.__dict__["_content"].append(
-                _maybe_wrap(
-                    annotated_type=self._metadata.element_type,
+            index = len(self)
+            self._validate_set(key=index, value=item)
+
+            node = _maybe_wrap(
+                ref_type=self._metadata.element_type,
+                key=index,
+                value=item,
+                is_optional=OmegaConf.is_optional(item),
+                parent=self,
+            )
+            self.__dict__["_content"].append(node)
+        except Exception as e:
+            self._format_and_raise(key=index, value=item, cause=e)
+            assert False
+
+    def _update_keys(self) -> None:
+        for i in range(len(self)):
+            node = self.get_node(i)
+            if node is not None:
+                node._metadata.key = i
+
+    def insert(self, index: int, item: Any) -> None:
+        from omegaconf.omegaconf import OmegaConf, _maybe_wrap
+
+        try:
+            if self._get_flag("readonly"):
+                raise ReadonlyConfigError("Cannot insert into a read-only ListConfig")
+            if self._is_none():
+                raise TypeError(
+                    "Cannot insert into ListConfig object representing None"
+                )
+            if self._is_missing():
+                raise MissingMandatoryValue("Cannot insert into missing ListConfig")
+
+            try:
+                assert isinstance(self._content, list)
+                # insert place holder
+                self._content.insert(index, None)
+                node = _maybe_wrap(
+                    ref_type=self._metadata.element_type,
                     key=index,
                     value=item,
                     is_optional=OmegaConf.is_optional(item),
                     parent=self,
                 )
-            )
-        except UnsupportedValueType:
-            full_key = self._get_full_key(f"{len(self)}")
-            raise UnsupportedValueType(
-                f"{type(item).__name__} is not a supported type (key: {full_key})"
-            )
-
-    def insert(self, index: int, item: Any) -> None:
-        if self._get_flag("readonly"):
-            raise ReadonlyConfigError(self._get_full_key(str(index)))
-        try:
-            self._content.insert(index, AnyNode(None))
-            self._set_at_index(index, item)
-        except Exception:
-            del self.__dict__["_content"][index]
-            raise
+                self._validate_set(key=index, value=node)
+                self._set_at_index(index, node)
+                self._update_keys()
+            except Exception:
+                del self.__dict__["_content"][index]
+                self._update_keys()
+                raise
+        except Exception as e:
+            self._format_and_raise(key=index, value=item, cause=e)
+            assert False
 
     def extend(self, lst: Iterable[Any]) -> None:
         assert isinstance(lst, (tuple, list, ListConfig))
@@ -152,6 +194,18 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
     def remove(self, x: Any) -> None:
         del self[self.index(x)]
 
+    def __delitem__(self, key: Union[str, int, slice]) -> None:
+        if self._get_flag("readonly"):
+            self._format_and_raise(
+                key=key,
+                value=None,
+                cause=ReadonlyConfigError(
+                    "Cannot delete item from read-only ListConfig"
+                ),
+            )
+        del self.__dict__["_content"][key]
+        self._update_keys()
+
     def clear(self) -> None:
         del self[:]
 
@@ -173,7 +227,10 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
         if found_idx != -1:
             return found_idx
         else:
-            raise ValueError("Item not found in ListConfig")
+            self._format_and_raise(
+                key=None, value=None, cause=ValueError("Item not found in ListConfig")
+            )
+            assert False
 
     def count(self, x: Any) -> int:
         c = 0
@@ -185,42 +242,89 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
     def copy(self) -> "ListConfig":
         return copy.copy(self)
 
-    def get_node(self, index: int) -> Node:
-        assert type(index) == int
-        return self.__dict__["_content"][index]  # type: ignore
+    def get_node(self, key: Any) -> Optional[Node]:
+        return self.get_node_ex(key)
+
+    def get_node_ex(self, key: Any, validate_access: bool = True) -> Optional[Node]:
+        if self._is_none():
+            raise TypeError(
+                "Cannot get_node from a ListConfig object representing None"
+            )
+        if self._is_missing():
+            raise MissingMandatoryValue("Cannot get_node from a missing ListConfig")
+
+        try:
+            assert isinstance(self._content, list)
+            return self._content[key]  # type: ignore
+        except (IndexError, TypeError) as e:
+            if validate_access:
+                self._format_and_raise(key=key, value=None, cause=e)
+                assert False
+            else:
+                return None
 
     def get(self, index: int, default_value: Any = None) -> Any:
-        assert type(index) == int
-        return self._resolve_with_default(
-            key=index, value=self._content[index], default_value=default_value
-        )
+        try:
+            if self._is_none():
+                raise TypeError("Cannot get from a ListConfig object representing None")
+            if self._is_missing():
+                raise MissingMandatoryValue("Cannot get from a missing ListConfig")
+            self._validate_get(index, None)
+            assert isinstance(self._content, list)
+            return self._resolve_with_default(
+                key=index, value=self._content[index], default_value=default_value
+            )
+        except Exception as e:
+            self._format_and_raise(key=index, value=None, cause=e)
+            assert False
 
     def pop(self, index: int = -1) -> Any:
-        if self._get_flag("readonly"):
-            raise ReadonlyConfigError(
-                self._get_full_key(str(index if index != -1 else ""))
+        try:
+            if self._get_flag("readonly"):
+                raise ReadonlyConfigError("Cannot pop from read-only ListConfig")
+            if self._is_none():
+                raise TypeError("Cannot pop from a ListConfig object representing None")
+            if self._is_missing():
+                raise MissingMandatoryValue("Cannot pop from a missing ListConfig")
+
+            assert isinstance(self._content, list)
+            ret = self._resolve_with_default(
+                key=index, value=self.get_node(index), default_value=None
             )
-        return self._resolve_with_default(
-            key=index, value=self._content.pop(index), default_value=None
-        )
+            del self._content[index]
+            self._update_keys()
+            return ret
+        except (ReadonlyConfigError, IndexError) as e:
+            self._format_and_raise(key=index, value=None, cause=e)
+            assert False
 
     def sort(
         self, key: Optional[Callable[[Any], Any]] = None, reverse: bool = False
     ) -> None:
-        if self._get_flag("readonly"):
-            raise ReadonlyConfigError()
+        try:
+            if self._get_flag("readonly"):
+                raise ReadonlyConfigError("Cannot sort a read-only ListConfig")
+            if self._is_none():
+                raise TypeError("Cannot sort a ListConfig object representing None")
+            if self._is_missing():
+                raise MissingMandatoryValue("Cannot sort a missing ListConfig")
 
-        if key is None:
+            if key is None:
 
-            def key1(x: Any) -> Any:
-                return x._value()
+                def key1(x: Any) -> Any:
+                    return x._value()
 
-        else:
+            else:
+
+                def key1(x: Any) -> Any:
+                    return key(x._value())  # type: ignore
 
-            def key1(x: Any) -> Any:
-                return key(x._value())  # type: ignore
+            assert isinstance(self._content, list)
+            self._content.sort(key=key1, reverse=reverse)
 
-        self._content.sort(key=key1, reverse=reverse)
+        except (ReadonlyConfigError, IndexError) as e:
+            self._format_and_raise(key=None, value=None, cause=e)
+            assert False
 
     def __eq__(self, other: Any) -> bool:
         if isinstance(other, (list, tuple)) or other is None:
@@ -239,21 +343,31 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
         return hash(str(self))
 
     def __iter__(self) -> Iterator[Any]:
-        class MyItems(Iterator[Any]):
-            def __init__(self, lst: List[Any]) -> None:
-                self.lst = lst
-                self.iterator = iter(lst)
-
-            def __next__(self) -> Any:
-                return self.next()
-
-            def next(self) -> Any:
-                v = next(self.iterator)
-                if isinstance(v, ValueNode):
-                    v = v._value()
-                return v
-
-        return MyItems(self._content)
+        try:
+            if self._is_none():
+                raise TypeError("Cannot iterate on ListConfig object representing None")
+            if self._is_missing():
+                raise MissingMandatoryValue("Cannot iterate on a missing ListConfig")
+
+            class MyItems(Iterator[Any]):
+                def __init__(self, lst: List[Any]) -> None:
+                    self.lst = lst
+                    self.iterator = iter(lst)
+
+                def __next__(self) -> Any:
+                    return self.next()
+
+                def next(self) -> Any:
+                    v = next(self.iterator)
+                    if isinstance(v, ValueNode):
+                        v = v._value()
+                    return v
+
+            assert isinstance(self._content, list)
+            return MyItems(self._content)
+        except (ReadonlyConfigError, TypeError, MissingMandatoryValue) as e:
+            self._format_and_raise(key=None, value=None, cause=e)
+            assert False
 
     def __add__(self, other: Union[List[Any], "ListConfig"]) -> "ListConfig":
         # res is sharing this list's parent to allow interpolation to work as expected
@@ -291,9 +405,15 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
         else:
             assert is_primitive_list(value) or isinstance(value, ListConfig)
             self.__dict__["_content"] = []
+            if isinstance(value, ListConfig):
+                self._metadata = copy.deepcopy(value._metadata)
+                self._metadata.flags = {}
             for item in value:
                 self.append(item)
 
+            if isinstance(value, ListConfig):
+                self._metadata.flags = value._metadata.flags
+
     @staticmethod
     def _list_eq(l1: Optional["ListConfig"], l2: Optional["ListConfig"]) -> bool:
 
diff --git a/omegaconf/nodes.py b/omegaconf/nodes.py
index edd3a08..fde6d65 100644
--- a/omegaconf/nodes.py
+++ b/omegaconf/nodes.py
@@ -1,9 +1,10 @@
 import copy
 import math
+import sys
 from enum import Enum
 from typing import Any, Dict, Optional, Type, Union
 
-from omegaconf._utils import _is_interpolation
+from omegaconf._utils import _is_interpolation, get_type_of
 from omegaconf.base import Container, Metadata, Node
 from omegaconf.errors import (
     MissingMandatoryValue,
@@ -15,12 +16,8 @@ from omegaconf.errors import (
 class ValueNode(Node):
     _val: Any
 
-    def __init__(
-        self, parent: Optional[Container], value: Any, key: Any, is_optional: bool
-    ):
-        super().__init__(
-            parent=parent, metadata=Metadata(key=key, optional=is_optional)
-        )
+    def __init__(self, parent: Optional[Container], value: Any, metadata: Metadata):
+        super().__init__(parent=parent, metadata=metadata)
         self._set_value(value)
 
     def _value(self) -> Any:
@@ -95,6 +92,16 @@ class ValueNode(Node):
     def _is_interpolation(self) -> bool:
         return _is_interpolation(self._value())
 
+    def _get_full_key(self, key: Union[str, Enum, int, None]) -> str:
+        parent = self._get_parent()
+        if parent is None:
+            if self._metadata.key is None:
+                return ""
+            else:
+                return str(self._metadata.key)
+        else:
+            return parent._get_full_key(self._metadata.key)
+
 
 class AnyNode(ValueNode):
     def __init__(
@@ -104,14 +111,21 @@ class AnyNode(ValueNode):
         parent: Optional[Container] = None,
         is_optional: bool = True,
     ):
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                ref_type=None, object_type=None, key=key, optional=is_optional
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Any:
-        from ._utils import _is_primitive_type
+        from ._utils import is_primitive_type
 
-        if not _is_primitive_type(value):
+        if not is_primitive_type(value):
+            t = get_type_of(value)
             raise UnsupportedValueType(
-                f"Unsupported value type, type={type(value)}, value={value}"
+                f"Value '{t.__name__}' is not a supported primitive type"
             )
         return value
 
@@ -129,7 +143,13 @@ class StringNode(ValueNode):
         parent: Optional[Container] = None,
         is_optional: bool = True,
     ):
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                key=key, optional=is_optional, ref_type=str, object_type=str
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Optional[str]:
         return str(value) if value is not None else None
@@ -148,7 +168,13 @@ class IntegerNode(ValueNode):
         parent: Optional[Container] = None,
         is_optional: bool = True,
     ):
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                key=key, optional=is_optional, ref_type=int, object_type=int
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Optional[int]:
         try:
@@ -159,9 +185,7 @@ class IntegerNode(ValueNode):
             else:
                 raise ValueError()
         except ValueError:
-            raise ValidationError(
-                f"Value '{value}' could not be converted to Integer"
-            ) from None
+            raise ValidationError("Value '$VALUE' could not be converted to Integer")
         return val
 
     def __deepcopy__(self, memo: Dict[int, Any] = {}) -> "IntegerNode":
@@ -178,7 +202,13 @@ class FloatNode(ValueNode):
         parent: Optional[Container] = None,
         is_optional: bool = True,
     ):
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                key=key, optional=is_optional, ref_type=float, object_type=float
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Optional[float]:
         if value is None:
@@ -189,9 +219,7 @@ class FloatNode(ValueNode):
             else:
                 raise ValueError()
         except ValueError:
-            raise ValidationError(
-                f"Value '{value}' could not be converted to float"
-            ) from None
+            raise ValidationError("Value '$VALUE' could not be converted to Float")
 
     def __eq__(self, other: Any) -> bool:
         if isinstance(other, ValueNode):
@@ -225,7 +253,13 @@ class BooleanNode(ValueNode):
         parent: Optional[Container] = None,
         is_optional: bool = True,
     ):
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                key=key, optional=is_optional, ref_type=bool, object_type=bool
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Optional[bool]:
         if isinstance(value, bool):
@@ -237,18 +271,18 @@ class BooleanNode(ValueNode):
         elif isinstance(value, str):
             try:
                 return self.validate_and_convert(int(value))
-            except ValueError:
+            except ValueError as e:
                 if value.lower() in ("yes", "y", "on", "true"):
                     return True
                 elif value.lower() in ("no", "n", "off", "false"):
                     return False
                 else:
                     raise ValidationError(
-                        "Value '{}' is not a valid bool".format(value)
-                    ) from None
+                        "Value '$VALUE' is not a valid bool (type $VALUE_TYPE)"
+                    ).with_traceback(sys.exc_info()[2]) from e
         else:
             raise ValidationError(
-                f"Value '{value}' is not a valid bool (type {type(value).__name__})"
+                "Value '$VALUE' is not a valid bool (type $VALUE_TYPE)"
             )
 
     def __deepcopy__(self, memo: Dict[int, Any] = {}) -> "BooleanNode":
@@ -281,7 +315,13 @@ class EnumNode(ValueNode):  # lgtm [py/missing-equals] : Intentional.
         self.enum_type: Type[Enum] = enum_type
         for name, constant in enum_type.__members__.items():
             self.fields[name] = constant.value
-        super().__init__(parent=parent, is_optional=is_optional, value=value, key=key)
+        super().__init__(
+            parent=parent,
+            value=value,
+            metadata=Metadata(
+                key=key, optional=is_optional, ref_type=enum_type, object_type=enum_type
+            ),
+        )
 
     def validate_and_convert(self, value: Any) -> Optional[Enum]:
         return self.validate_and_convert_to_enum(enum_type=self.enum_type, value=value)
@@ -294,9 +334,8 @@ class EnumNode(ValueNode):  # lgtm [py/missing-equals] : Intentional.
             return None
 
         if not isinstance(value, (str, int)) and not isinstance(value, enum_type):
-            # if type(value) not in (str, int) and not isinstance(value, enum_type):
             raise ValidationError(
-                f"Value {value} ({type(value).__name__}) is not a valid input for {enum_type}"
+                f"Value $VALUE ($VALUE_TYPE) is not a valid input for {enum_type}"
             )
 
         if isinstance(value, enum_type):
@@ -310,18 +349,18 @@ class EnumNode(ValueNode):  # lgtm [py/missing-equals] : Intentional.
                 return enum_type(value)
 
             if isinstance(value, str):
-                prefix = "{}.".format(enum_type.__name__)
+                prefix = f"{enum_type.__name__}."
                 if value.startswith(prefix):
                     value = value[len(prefix) :]
                 return enum_type[value]
 
-            assert False  # pragma: no cover
+            assert False
 
-        except (ValueError, KeyError):
+        except (ValueError, KeyError) as e:
             valid = "\n".join([f"\t{x}" for x in enum_type.__members__.keys()])
             raise ValidationError(
-                f"Invalid value '{value}', expected one of:\n{valid}"
-            ) from None
+                f"Invalid value '$VALUE', expected one of:\n{valid}"
+            ).with_traceback(sys.exc_info()[2]) from e
 
     def __deepcopy__(self, memo: Dict[int, Any] = {}) -> "EnumNode":
         res = EnumNode(enum_type=self.enum_type)
diff --git a/omegaconf/omegaconf.py b/omegaconf/omegaconf.py
index ee31bbf..e609aeb 100644
--- a/omegaconf/omegaconf.py
+++ b/omegaconf/omegaconf.py
@@ -29,16 +29,16 @@ from typing_extensions import Protocol
 from . import DictConfig, ListConfig
 from ._utils import (
     ValueKind,
-    _get_key_value_types,
-    _valid_value_annotation_type,
     decode_primitive,
     get_type_of,
     get_value_kind,
     is_primitive_container,
     is_primitive_dict,
     is_primitive_list,
+    is_primitive_type,
     is_structured_config,
     isint,
+    valid_value_annotation_type,
 )
 from .base import Container, Node
 from .basecontainer import BaseContainer
@@ -109,7 +109,7 @@ def register_default_resolvers() -> None:
             if default is not None:
                 return decode_primitive(default)
             else:
-                raise KeyError("Environment variable '{}' not found".format(key))
+                raise ValidationError(f"Environment variable '{key}' not found")
 
     OmegaConf.register_resolver("env", env)
 
@@ -148,6 +148,12 @@ class OmegaConf:
     @staticmethod
     def create(  # noqa F811
         obj: Any = _EMPTY_MARKER_, parent: Optional[BaseContainer] = None
+    ) -> Union[DictConfig, ListConfig]:
+        return OmegaConf._create_impl(obj=obj, parent=parent)
+
+    @staticmethod
+    def _create_impl(  # noqa F811
+        obj: Any = _EMPTY_MARKER_, parent: Optional[BaseContainer] = None
     ) -> Union[DictConfig, ListConfig]:
         from ._utils import get_yaml_loader
         from .dictconfig import DictConfig
@@ -172,17 +178,11 @@ class OmegaConf:
                 or is_structured_config(obj)
                 or obj is None
             ):
-                key_type, element_type = _get_key_value_types(obj)
-                annotated_type = OmegaConf.get_type(obj)
-                return DictConfig(
-                    content=obj,
-                    parent=parent,
-                    annotated_type=annotated_type,
-                    key_type=key_type,
-                    element_type=element_type,
-                )
+                ref_type = OmegaConf.get_type(obj)
+                return DictConfig(content=obj, parent=parent, ref_type=ref_type)
             elif is_primitive_list(obj) or OmegaConf.is_list(obj):
-                return ListConfig(obj, parent)
+                ref_type = OmegaConf.get_type(obj)
+                return ListConfig(ref_type=ref_type, content=obj, parent=parent)
             else:
                 if isinstance(obj, type):
                     raise ValidationError(
@@ -191,7 +191,7 @@ class OmegaConf:
                     )
                 else:
                     raise ValidationError(
-                        "Unsupported type {}".format(type(obj).__name__)
+                        f"Object of unsupported type: '{type(obj).__name__}'"
                     )
 
     @staticmethod
@@ -260,7 +260,8 @@ class OmegaConf:
         if is_primitive_container(target) or is_structured_config(target):
             target = OmegaConf.create(target)
         assert isinstance(target, (DictConfig, ListConfig))
-        target.merge_with(*others[1:])
+        with flag_override(target, "readonly", False):
+            target.merge_with(*others[1:])
         return target
 
     @staticmethod
@@ -382,6 +383,8 @@ class OmegaConf:
     def is_missing(cfg: BaseContainer, key: Union[int, str]) -> bool:
         try:
             node = cfg.get_node(key)
+            if node is None:
+                return False
             return node._is_missing()
         except (UnsupportedInterpolationType, KeyError, AttributeError):
             return False
@@ -437,9 +440,13 @@ class OmegaConf:
 
     @staticmethod
     def get_type(obj: Any, key: Optional[str] = None) -> Optional[Type[Any]]:
-
         if is_structured_config(obj):
             return get_type_of(obj)
+        else:
+            if obj is None:
+                return None
+            if is_primitive_type(obj):
+                return get_type_of(obj)
 
         if key is not None:
             c = obj.get_node(key)
@@ -447,9 +454,35 @@ class OmegaConf:
             c = obj
 
         if isinstance(c, DictConfig):
-            t = c._metadata.object_type
-            assert t is None or isinstance(t, type)
-            return t
+            if c._is_none():
+                return None
+            elif c._is_missing():
+                return None
+            else:
+                if is_structured_config(c._metadata.object_type):
+                    return c._metadata.object_type
+                else:
+                    return dict
+        elif isinstance(c, ListConfig):
+            return list
+        elif isinstance(c, ValueNode):
+            return type(c._value())
+        elif isinstance(c, dict):
+            return dict
+        elif isinstance(c, (list, tuple)):
+            return list
+        else:
+            assert False
+
+    @staticmethod
+    def get_ref_type(obj: Any, key: Optional[str] = None) -> Optional[Type[Any]]:
+        if key is not None:
+            c = obj.get_node(key)
+        else:
+            c = obj
+
+        if isinstance(c, Node):
+            return c._metadata.ref_type
         else:
             return None
 
@@ -500,7 +533,7 @@ def open_dict(config: Container) -> Generator[Container, None, None]:
 def _node_wrap(
     type_: Any, parent: Optional[BaseContainer], is_optional: bool, value: Any, key: Any
 ) -> ValueNode:
-    if not _valid_value_annotation_type(type_):
+    if not valid_value_annotation_type(type_):
         raise ValidationError(
             f"Annotated class '{type_.__name__}' is not a structured config. "
             "did you forget to decorate it as a dataclass?"
@@ -525,12 +558,14 @@ def _node_wrap(
     elif type_ == str:
         node = StringNode(value=value, key=key, parent=parent, is_optional=is_optional)
     else:
-        raise ValidationError(f"Unexpected object type : {type_.__name__}")
+        raise ValidationError(
+            f"Unexpected object type : {type_.__name__}"
+        )  # pragma: no cover
     return node
 
 
 def _maybe_wrap(
-    annotated_type: Any,
+    ref_type: Any,
     key: Any,
     value: Any,
     is_optional: bool,
@@ -539,9 +574,11 @@ def _maybe_wrap(
     from . import DictConfig, ListConfig
 
     if isinstance(value, ValueNode):
+        value._set_key(key)
+        value._set_parent(parent)
         return value
     ret: Node
-    origin_ = getattr(annotated_type, "__origin__", None)
+    origin_ = getattr(ref_type, "__origin__", None)
     is_dict = (
         type(value) in (dict, DictConfig)
         or origin_ in (dict, DictConfig)
@@ -555,47 +592,35 @@ def _maybe_wrap(
     value_kind = get_value_kind(value)
 
     if is_dict:
-        key_type, element_type = _get_key_value_types(annotated_type)
         ret = DictConfig(
             content=value,
             key=key,
             parent=parent,
-            annotated_type=None,
+            ref_type=ref_type,
             is_optional=is_optional,
-            key_type=key_type,
-            element_type=element_type,
         )
     elif is_list:
-        args = getattr(annotated_type, "__args__", None)
-        if annotated_type is not List and args is not None:
-            element_type = args[0]
-        else:
-            element_type = Any
-
-        if not (_valid_value_annotation_type(element_type)):
-            raise ValidationError(f"Unsupported value type : {element_type}")
 
         ret = ListConfig(
             content=value,
             key=key,
             parent=parent,
             is_optional=is_optional,
-            element_type=element_type,
+            ref_type=ref_type,
         )
-
     elif (
-        is_structured_config(annotated_type)
+        is_structured_config(ref_type)
         and (
             is_structured_config(value)
             or value_kind == ValueKind.MANDATORY_MISSING
             or value_kind == ValueKind.INTERPOLATION
             or value is None
         )
-    ) or is_structured_config(value):
+    ) or is_structured_config(ref_type):
         from . import DictConfig
 
         ret = DictConfig(
-            annotated_type=annotated_type,
+            ref_type=ref_type,
             is_optional=is_optional,
             content=value,
             key=key,
@@ -603,7 +628,7 @@ def _maybe_wrap(
         )
     else:
         ret = _node_wrap(
-            type_=annotated_type,
+            type_=ref_type,
             parent=parent,
             is_optional=is_optional,
             value=value,
@@ -614,7 +639,7 @@ def _maybe_wrap(
 
 
 def _select_one(
-    c: Container, key: str, throw_on_missing: bool
+    c: Container, key: str, throw_on_missing: bool, throw_on_type_error: bool = True
 ) -> Tuple[Optional[Node], Union[str, int]]:
     from .dictconfig import DictConfig
     from .listconfig import ListConfig
@@ -623,12 +648,13 @@ def _select_one(
     assert isinstance(c, (DictConfig, ListConfig)), f"Unexpected type : {c}"
     if isinstance(c, DictConfig):
         assert isinstance(ret_key, str)
-        val: Optional[Node]
-        if c.get_node_ex(ret_key, validate_access=False) is not None:
-            val = c.get_node(ret_key)
+        val: Optional[Node] = c.get_node_ex(ret_key, validate_access=False)
+        if val is not None:
             if val._is_missing():
                 if throw_on_missing:
-                    raise MissingMandatoryValue(c._get_full_key(ret_key))
+                    raise MissingMandatoryValue(
+                        f"Missing mandatory value : {c._get_full_key(ret_key)}"
+                    )
                 else:
                     return val, ret_key
         else:
@@ -636,13 +662,19 @@ def _select_one(
     elif isinstance(c, ListConfig):
         assert isinstance(ret_key, str)
         if not isint(ret_key):
-            raise TypeError("Index {} is not an int".format(ret_key))
-        ret_key = int(ret_key)
-        if ret_key < 0 or ret_key + 1 > len(c):
-            val = None
+            if throw_on_type_error:
+                raise TypeError(
+                    f"Index '{ret_key}' ({type(ret_key).__name__}) is not an int"
+                )
+            else:
+                val = None
         else:
-            val = c.get_node(ret_key)
+            ret_key = int(ret_key)
+            if ret_key < 0 or ret_key + 1 > len(c):
+                val = None
+            else:
+                val = c.get_node(ret_key)
     else:
-        assert False  # pragma: no cover
+        assert False
 
     return val, ret_key


Test patch:
diff --git a/tests/examples/test_dataclass_example.py b/tests/examples/test_dataclass_example.py
index 14c09c2..4e30214 100644
--- a/tests/examples/test_dataclass_example.py
+++ b/tests/examples/test_dataclass_example.py
@@ -337,7 +337,7 @@ domains:
 
     schema: Config = OmegaConf.structured(Config)
     cfg = OmegaConf.create(yaml)
-    merged = OmegaConf.merge(schema, cfg)
+    merged: Any = OmegaConf.merge(schema, cfg)
     assert merged == {
         "num": 10,
         "user": {"name": "Omry", "height": "???"},
diff --git a/tests/structured_conf/data/attr_classes.py b/tests/structured_conf/data/attr_classes.py
index 6afa825..8e088c4 100644
--- a/tests/structured_conf/data/attr_classes.py
+++ b/tests/structured_conf/data/attr_classes.py
@@ -360,7 +360,14 @@ class FaultyPlugin:
     name: str = "faulty_plugin"
 
 
-# Missing
+@attr.s(auto_attribs=True)
+class PluginHolder:
+    none: Optional[Plugin] = None
+    missing: Plugin = MISSING
+    plugin: Plugin = Plugin()
+    plugin2: Plugin = ConcretePlugin()
+
+
 @attr.s(auto_attribs=True)
 class LinkedList:
     next: Optional["LinkedList"] = None
diff --git a/tests/structured_conf/data/dataclasses.py b/tests/structured_conf/data/dataclasses.py
index 2b4dad0..282903b 100644
--- a/tests/structured_conf/data/dataclasses.py
+++ b/tests/structured_conf/data/dataclasses.py
@@ -367,7 +367,14 @@ class FaultyPlugin:
     name: str = "faulty_plugin"
 
 
-# Missing
+@dataclass
+class PluginHolder:
+    none: Optional[Plugin] = None
+    missing: Plugin = MISSING
+    plugin: Plugin = Plugin()
+    plugin2: Plugin = ConcretePlugin()
+
+
 @dataclass
 class LinkedList:
     next: Optional["LinkedList"] = None
diff --git a/tests/structured_conf/test_structured_basic.py b/tests/structured_conf/test_structured_basic.py
index 138e4d3..d9f710e 100644
--- a/tests/structured_conf/test_structured_basic.py
+++ b/tests/structured_conf/test_structured_basic.py
@@ -49,7 +49,7 @@ class TestStructured:
             cfg1 = OmegaConf.create({"plugin": module.Plugin})
             cfg2 = OmegaConf.create({"plugin": module.ConcretePlugin})
             assert cfg2.plugin == module.ConcretePlugin
-            res = OmegaConf.merge(cfg1, cfg2)
+            res: Any = OmegaConf.merge(cfg1, cfg2)
             assert OmegaConf.get_type(res.plugin) == module.ConcretePlugin
             assert (
                 OmegaConf.get_type(res.plugin.params)
@@ -66,7 +66,7 @@ class TestStructured:
         def test_error_message(self, class_type: str) -> None:
             module: Any = import_module(class_type)
             cfg = OmegaConf.structured(module.StructuredOptional)
-            msg = re.escape("Cannot assign Nested=None (field is not Optional)")
+            msg = re.escape("field 'not_optional' is not Optional")
             with pytest.raises(ValidationError, match=msg):
                 cfg.not_optional = None
 
@@ -98,15 +98,18 @@ class TestStructured:
 
     def test_get_type(self, class_type: str) -> None:
         module: Any = import_module(class_type)
-        linked_list = module.LinkedList
-        cfg1 = OmegaConf.create(linked_list)
-        assert OmegaConf.get_type(cfg1) == linked_list
+        cfg1 = OmegaConf.create(module.LinkedList)
+        assert OmegaConf.get_type(cfg1) == module.LinkedList
+        assert OmegaConf.get_ref_type(cfg1, "next") == module.LinkedList
+        assert OmegaConf.get_type(cfg1, "next") is None
+
         assert cfg1.next is None
         assert OmegaConf.is_missing(cfg1, "value")
 
         cfg2 = OmegaConf.create(module.MissingTest.Missing1)
         assert OmegaConf.is_missing(cfg2, "head")
-        assert OmegaConf.get_type(cfg2, "head") == module.LinkedList
+        assert OmegaConf.get_ref_type(cfg2, "head") == module.LinkedList
+        assert OmegaConf.get_type(cfg2, "head") is None
 
     class TestMissing:
         def test_missing1(self, class_type: str) -> None:
@@ -114,7 +117,7 @@ class TestStructured:
             cfg = OmegaConf.create(module.MissingTest.Missing1)
             assert OmegaConf.is_missing(cfg, "head")
 
-            assert OmegaConf.get_type(cfg, "head") == module.LinkedList
+            assert OmegaConf.get_type(cfg, "head") is None
 
             with pytest.raises(ValidationError):
                 cfg.head = 10
@@ -127,3 +130,21 @@ class TestStructured:
 
             cfg.head.next = module.LinkedList(value=2)
             assert cfg == {"head": {"next": {"next": None, "value": 2}, "value": 1}}
+
+        def test_plugin_holder(self, class_type: str) -> None:
+            module: Any = import_module(class_type)
+            cfg = OmegaConf.create(module.PluginHolder)
+            assert OmegaConf.get_ref_type(cfg, "none") == module.Plugin
+            assert OmegaConf.get_type(cfg, "none") is None
+
+            assert OmegaConf.get_ref_type(cfg, "missing") == module.Plugin
+            assert OmegaConf.get_type(cfg, "missing") is None
+
+            assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
+            assert OmegaConf.get_type(cfg, "plugin") == module.Plugin
+            cfg.plugin = module.ConcretePlugin()
+            assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
+            assert OmegaConf.get_type(cfg, "plugin") == module.ConcretePlugin
+
+            assert OmegaConf.get_ref_type(cfg, "plugin2") == module.Plugin
+            assert OmegaConf.get_type(cfg, "plugin2") == module.ConcretePlugin
diff --git a/tests/structured_conf/test_structured_config.py b/tests/structured_conf/test_structured_config.py
index c63cf0a..fd40375 100644
--- a/tests/structured_conf/test_structured_config.py
+++ b/tests/structured_conf/test_structured_config.py
@@ -94,7 +94,8 @@ class TestConfigs:
         module: Any = import_module(class_type)
         cfg = OmegaConf.structured(module.NestedWithNone)
         assert cfg == {"plugin": None}
-        assert OmegaConf.get_type(cfg, "plugin") == module.Plugin
+        assert OmegaConf.get_type(cfg, "plugin") is None
+        assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
 
     def test_nested_config(self, class_type: str) -> None:
         module: Any = import_module(class_type)
@@ -116,11 +117,15 @@ class TestConfigs:
                 "value_at_root": 1000,
             }
 
+            with pytest.raises(ValidationError):
+                cfg.user_provided_default = 10
+
             with pytest.raises(ValidationError):
                 cfg.default_value = 10
 
-            conf1.default_value = module.NestedSubclass()
-            assert conf1.default_value == {
+            # assign subclass
+            cfg.default_value = module.NestedSubclass()
+            assert cfg.default_value == {
                 "additional": 20,
                 "with_default": 10,
                 "null_default": None,
@@ -128,8 +133,9 @@ class TestConfigs:
                 "interpolation": "${value_at_root}",
             }
 
-            with pytest.raises(ValidationError):
-                cfg.default_value = module.Nested()
+            # assign original ref type back
+            cfg.default_value = module.Nested()
+            assert cfg.default_value == module.Nested()
 
         conf1 = OmegaConf.structured(module.NestedConfig)
         validate(conf1)
@@ -418,10 +424,9 @@ class TestConfigs:
     def test_merged_with_subclass(self, class_type: str) -> None:
         # Test that the merged type is that of the last merged config
         module: Any = import_module(class_type)
-        res = OmegaConf.merge(
-            OmegaConf.structured(module.Nested),
-            OmegaConf.structured(module.NestedSubclass),
-        )
+        c1 = OmegaConf.structured(module.Nested)
+        c2 = OmegaConf.structured(module.NestedSubclass)
+        res = OmegaConf.merge(c1, c2)
         assert OmegaConf.get_type(res) == module.NestedSubclass
 
     def test_typed_dict_key_error(self, class_type: str) -> None:
@@ -680,6 +685,12 @@ class TestDictSubclass:
     def test_color2color(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         cfg = OmegaConf.structured(module.DictSubclass.Color2Color())
+
+        # add key
+        cfg[Color.RED] = "GREEN"
+        assert cfg[Color.RED] == Color.GREEN
+
+        # replace key
         cfg[Color.RED] = "RED"
         assert cfg[Color.RED] == Color.RED
 
diff --git a/tests/test_base_config.py b/tests/test_base_config.py
index 67c7b89..fd936a0 100644
--- a/tests/test_base_config.py
+++ b/tests/test_base_config.py
@@ -261,11 +261,7 @@ def test_deepcopy_and_merge_and_flags() -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "cfg",
-    [
-        ListConfig(content=[], element_type=int),
-        DictConfig(content={}, element_type=int),
-    ],
+    "cfg", [ListConfig(ref_type=List[int], content=[]), DictConfig(content={})],
 )
 def test_deepcopy_preserves_container_type(cfg: Container) -> None:
     cp: Container = copy.deepcopy(cfg)
@@ -273,27 +269,33 @@ def test_deepcopy_preserves_container_type(cfg: Container) -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "src, flag_name, flag_value, func, expectation",
+    "src, flag_name, func, expectation",
     [
-        ({}, "struct", False, lambda c: c.__setitem__("foo", 1), raises(KeyError),),
-        (
+        pytest.param(
+            {},
+            "struct",
+            lambda c: c.__setitem__("foo", 1),
+            raises(KeyError),
+            id="struct_setiitem",
+        ),
+        pytest.param(
             {},
             "struct",
-            False,
             lambda c: c.__setattr__("foo", 1),
             raises(AttributeError),
+            id="struct_setattr",
         ),
-        (
+        pytest.param(
             {},
             "readonly",
-            False,
             lambda c: c.__setitem__("foo", 1),
             raises(ReadonlyConfigError),
+            id="readonly",
         ),
     ],
 )
 def test_flag_override(
-    src: Dict[str, Any], flag_name: str, flag_value: bool, func: Any, expectation: Any
+    src: Dict[str, Any], flag_name: str, func: Any, expectation: Any
 ) -> None:
     c = OmegaConf.create(src)
     c._set_flag(flag_name, True)
@@ -301,7 +303,7 @@ def test_flag_override(
         func(c)
 
     with does_not_raise():
-        with flag_override(c, flag_name, flag_value):
+        with flag_override(c, flag_name, False):
             func(c)
 
 
@@ -450,7 +452,7 @@ def test_omegaconf_create() -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "parent, index, value, expected",
+    "parent, key, value, expected",
     [
         ([10, 11], 0, ["a", "b"], [["a", "b"], 11]),
         ([None], 0, {"foo": "bar"}, [{"foo": "bar"}]),
@@ -460,7 +462,7 @@ def test_omegaconf_create() -> None:
         ({}, "foo", OmegaConf.create({"foo": "bar"}), {"foo": {"foo": "bar"}}),
     ],
 )
-def test_assign(parent: Any, index: Union[str, int], value: Any, expected: Any) -> None:
+def test_assign(parent: Any, key: Union[str, int], value: Any, expected: Any) -> None:
     c = OmegaConf.create(parent)
-    c[index] = value
+    c[key] = value
     assert c == expected
diff --git a/tests/test_basic_ops_dict.py b/tests/test_basic_ops_dict.py
index aa46b86..d937168 100644
--- a/tests/test_basic_ops_dict.py
+++ b/tests/test_basic_ops_dict.py
@@ -7,14 +7,23 @@ import pytest
 
 from omegaconf import (
     DictConfig,
-    KeyValidationError,
     MissingMandatoryValue,
     OmegaConf,
     UnsupportedValueType,
+    ValidationError,
 )
 from omegaconf.basecontainer import BaseContainer
-
-from . import Enum1, IllegalType, StructuredWithMissing, User, does_not_raise
+from omegaconf.errors import KeyValidationError
+
+from . import (
+    ConcretePlugin,
+    Enum1,
+    IllegalType,
+    Plugin,
+    StructuredWithMissing,
+    User,
+    does_not_raise,
+)
 
 
 def test_setattr_deep_value() -> None:
@@ -26,7 +35,7 @@ def test_setattr_deep_value() -> None:
 def test_setattr_deep_from_empty() -> None:
     c = OmegaConf.create()
     # Unfortunately we can't just do c.a.b = 9 here.
-    # The reason is that if c.a is being resolved first and it does not exist, so there
+    # The reason is that if c.a is being accessed first and it does not exist, so there
     # is nothing to call .b = 9 on.
     # The alternative is to auto-create fields as they are being accessed, but this is opening
     # a whole new can of worms, and is also breaking map semantics.
@@ -43,11 +52,13 @@ def test_setattr_deep_map() -> None:
 
 def test_getattr() -> None:
     c = OmegaConf.create("a: b")
+    assert isinstance(c, DictConfig)
     assert "b" == c.a
 
 
 def test_getattr_dict() -> None:
     c = OmegaConf.create("a: {b: 1}")
+    assert isinstance(c, DictConfig)
     assert {"b": 1} == c.a
 
 
@@ -122,6 +133,7 @@ def test_get_default_value() -> None:
 
 def test_scientific_notation_float() -> None:
     c = OmegaConf.create("a: 10e-3")
+    assert isinstance(c, DictConfig)
     assert 10e-3 == c.a
 
 
@@ -178,6 +190,7 @@ def test_items_with_interpolation() -> None:
 
 def test_dict_keys() -> None:
     c = OmegaConf.create("{a: 2, b: 10}")
+    assert isinstance(c, DictConfig)
     assert {"a": 2, "b": 10}.keys() == c.keys()
 
 
@@ -217,39 +230,44 @@ def test_iterate_dictionary() -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "cfg, key, default_, expected, expectation",
+    "cfg, key, default_, expected",
     [
-        (dict(a=1, b=2), "a", None, 1, does_not_raise()),
-        (dict(a=1, b=2), "not_found", "default", "default", does_not_raise()),
-        (dict(a=1, b=2), "not_found", None, None, pytest.raises(KeyError)),
+        ({"a": 1, "b": 2}, "a", None, 1),
+        ({"a": 1, "b": 2}, "not_found", "default", "default"),
         # Interpolations
-        (dict(a="${b}", b=2), "a", None, 2, does_not_raise()),
-        (dict(a="???", b=2), "a", None, None, pytest.raises(KeyError)),
-        (
-            dict(a="${b}", b="???"),
-            "a",
-            None,
-            None,
-            pytest.raises(MissingMandatoryValue),
-        ),
+        ({"a": "${b}", "b": 2}, "a", None, 2),
         # enum key
-        ({Enum1.FOO: "bar"}, Enum1.FOO, None, "bar", does_not_raise()),
-        ({Enum1.FOO: "bar"}, Enum1.BAR, "default", "default", does_not_raise()),
-        ({Enum1.FOO: "bar"}, Enum1.BAR, None, None, pytest.raises(KeyError)),
+        ({Enum1.FOO: "bar"}, Enum1.FOO, None, "bar"),
+        ({Enum1.FOO: "bar"}, Enum1.BAR, "default", "default"),
     ],
 )
-def test_dict_pop(
-    cfg: Dict[Any, Any], key: Any, default_: Any, expected: Any, expectation: Any
-) -> None:
+def test_dict_pop(cfg: Dict[Any, Any], key: Any, default_: Any, expected: Any) -> None:
     c = OmegaConf.create(cfg)
-    with expectation:
-        if default_ is not None:
-            val = c.pop(key, default_)
-        else:
-            val = c.pop(key)
+    if default_ is not None:
+        val = c.pop(key, default_)
+    else:
+        val = c.pop(key)
+
+    assert val == expected
+    assert type(val) == type(expected)
 
-        assert val == expected
-        assert type(val) == type(expected)
+
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, key, expectation",
+    [
+        ({"a": 1, "b": 2}, "not_found", pytest.raises(KeyError)),
+        # Interpolations
+        ({"a": "???", "b": 2}, "a", pytest.raises(MissingMandatoryValue)),
+        ({"a": "${b}", "b": "???"}, "a", pytest.raises(MissingMandatoryValue),),
+        # enum key
+        ({Enum1.FOO: "bar"}, Enum1.BAR, pytest.raises(KeyError)),
+    ],
+)
+def test_dict_pop_error(cfg: Dict[Any, Any], key: Any, expectation: Any) -> None:
+    c = OmegaConf.create(cfg)
+    with expectation:
+        c.pop(key)
+    assert c == cfg
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -284,6 +302,7 @@ def test_get_root_of_merged() -> None:
 
     c2 = OmegaConf.create(dict(b=dict(b1="???", b2=4, bb=dict(bb1=3, bb2=4))))
     c3 = OmegaConf.merge(c1, c2)
+    assert isinstance(c3, DictConfig)
 
     assert c3._get_root() == c3
     assert c3.a._get_root() == c3
@@ -326,16 +345,16 @@ def test_dict_assign_illegal_value_nested() -> None:
 
 
 def test_assign_dict_in_dict() -> None:
-    c = OmegaConf.create(dict())
-    c.foo = dict(foo="bar")
-    assert c.foo == dict(foo="bar")
+    c = OmegaConf.create({})
+    c.foo = {"foo": "bar"}
+    assert c.foo == {"foo": "bar"}
     assert isinstance(c.foo, DictConfig)
 
 
 @pytest.mark.parametrize(  # type: ignore
     "src",
     [
-        dict(a=1, b=2, c=dict(aa=10)),
+        {"a": 1, "b": 2, "c": {"aa": 10}},
         [1, 2, 3],
         {"a": 1, "b": 2, "c": {"aa": 10, "lst": [1, 2, 3]}},
     ],
@@ -368,14 +387,23 @@ def test_pretty_with_resolve() -> None:
 
 def test_instantiate_config_fails() -> None:
     with pytest.raises(TypeError):
-        BaseContainer(element_type=Any, parent=None)  # type: ignore
+        BaseContainer()  # type: ignore
 
 
-def test_dir() -> None:
-    c1 = OmegaConf.create(dict(a=1, b=2, c=3))
-    assert dir(c1) == ["a", "b", "c"]
-    c2 = OmegaConf.structured(StructuredWithMissing)
-    assert dir(c2.get_node("dict")) == []
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, key, expected",
+    [
+        ({"a": 1, "b": 2, "c": 3}, None, ["a", "b", "c"]),
+        ({"a": {}}, "a", []),
+        (StructuredWithMissing, "dict", []),
+    ],
+)
+def test_dir(cfg: Any, key: Any, expected: Any) -> None:
+    c = OmegaConf.create(cfg)
+    if key is None:
+        assert dir(c) == expected
+    else:
+        assert dir(c.get_node(key)) == expected
 
 
 def test_hash() -> None:
@@ -476,8 +504,31 @@ def test_get_type() -> None:
     cfg = OmegaConf.structured(User(name="bond"))
     assert OmegaConf.get_type(cfg) == User
 
-    cfg = OmegaConf.create({"user": User})
+    cfg = OmegaConf.create({"user": User, "inter": "${user}"})
     assert OmegaConf.get_type(cfg.user) == User
+    assert OmegaConf.get_type(cfg.inter) == User
+
+
+def test_get_ref_type() -> None:
+    cfg = OmegaConf.create(
+        {"plugin": DictConfig(ref_type=Plugin, content=ConcretePlugin)}
+    )
+
+    assert OmegaConf.get_type(cfg.plugin) == ConcretePlugin
+    assert OmegaConf.get_ref_type(cfg.plugin) == Plugin
+
+
+def test_get_ref_type_with_conflict() -> None:
+    cfg = OmegaConf.create(
+        {"user": User, "inter": DictConfig(ref_type=Plugin, content="${user}")}
+    )
+
+    assert OmegaConf.get_type(cfg.user) == User
+    assert OmegaConf.get_ref_type(cfg.user) == User
+
+    # Interpolation inherits both type and ref type from the target
+    assert OmegaConf.get_type(cfg.inter) == User
+    assert OmegaConf.get_ref_type(cfg.inter) == User
 
 
 def test_is_missing() -> None:
@@ -490,8 +541,74 @@ def test_is_missing() -> None:
             "missing_node_inter": "${missing_node}",
         }
     )
-    assert cfg.get_node("foo")._is_missing()
-    assert cfg.get_node("inter")._is_missing()
-    assert cfg.get_node("str_inter")._is_missing()
-    assert cfg.get_node("missing_node")._is_missing()
-    assert cfg.get_node("missing_node_inter")._is_missing()
+    assert cfg.get_node("foo")._is_missing()  # type:ignore
+    assert cfg.get_node("inter")._is_missing()  # type:ignore
+    assert cfg.get_node("str_inter")._is_missing()  # type:ignore
+    assert cfg.get_node("missing_node")._is_missing()  # type:ignore
+    assert cfg.get_node("missing_node_inter")._is_missing()  # type:ignore
+
+
+@pytest.mark.parametrize("ref_type", [None, Any])  # type: ignore
+@pytest.mark.parametrize("assign", [None, {}, {"foo": "bar"}, [1, 2, 3]])  # type: ignore
+def test_assign_to_reftype_none_or_any(ref_type: Any, assign: Any) -> None:
+    cfg = OmegaConf.create({"foo": DictConfig(ref_type=ref_type, content={})})
+    cfg.foo = assign
+    assert cfg.foo == assign
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "ref_type,values,assign,expectation",
+    [
+        (Plugin, [None, "???", Plugin], None, does_not_raise),
+        (Plugin, [None, "???", Plugin], Plugin, does_not_raise),
+        (Plugin, [None, "???", Plugin], Plugin(), does_not_raise),
+        (Plugin, [None, "???", Plugin], ConcretePlugin, does_not_raise),
+        (Plugin, [None, "???", Plugin], ConcretePlugin(), does_not_raise),
+        (Plugin, [None, "???", Plugin], 10, lambda: pytest.raises(ValidationError)),
+        (ConcretePlugin, [None, "???", ConcretePlugin], None, does_not_raise),
+        (
+            ConcretePlugin,
+            [None, "???", ConcretePlugin],
+            Plugin,
+            lambda: pytest.raises(ValidationError),
+        ),
+        (
+            ConcretePlugin,
+            [None, "???", ConcretePlugin],
+            Plugin(),
+            lambda: pytest.raises(ValidationError),
+        ),
+        (
+            ConcretePlugin,
+            [None, "???", ConcretePlugin],
+            ConcretePlugin,
+            does_not_raise,
+        ),
+        (
+            ConcretePlugin,
+            [None, "???", ConcretePlugin],
+            ConcretePlugin(),
+            does_not_raise,
+        ),
+    ],
+)
+def test_assign_to_reftype_plugin(
+    ref_type: Any, values: List[Any], assign: Any, expectation: Any
+) -> None:
+    for value in values:
+        cfg = OmegaConf.create({"foo": DictConfig(ref_type=ref_type, content=value)})
+        with expectation():
+            assert OmegaConf.get_ref_type(cfg, "foo") == ref_type
+            cfg.foo = assign
+            assert cfg.foo == assign
+            # validate assignment does not change ref type.
+            assert OmegaConf.get_ref_type(cfg, "foo") == ref_type
+
+        if value is not None:
+            cfg = OmegaConf.create(
+                {"foo": DictConfig(ref_type=ref_type, content=value)}
+            )
+            with expectation():
+                cfg2 = OmegaConf.merge(cfg, {"foo": assign})
+                assert isinstance(cfg2, DictConfig)
+                assert cfg2.foo == assign
diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py
index 5e4038b..611fa51 100644
--- a/tests/test_basic_ops_list.py
+++ b/tests/test_basic_ops_list.py
@@ -5,7 +5,12 @@ from typing import Any, List, Optional
 import pytest
 
 from omegaconf import AnyNode, ListConfig, OmegaConf
-from omegaconf.errors import KeyValidationError, UnsupportedValueType
+from omegaconf.errors import (
+    KeyValidationError,
+    MissingMandatoryValue,
+    UnsupportedValueType,
+    ValidationError,
+)
 from omegaconf.nodes import IntegerNode, StringNode
 
 from . import IllegalType, does_not_raise
@@ -96,6 +101,23 @@ def test_list_pop() -> None:
     assert c == [2, 3]
     with pytest.raises(IndexError):
         c.pop(100)
+    validate_list_keys(c)
+
+
+def test_list_pop_on_unexpected_exception_not_modifying(mocker: Any) -> None:
+    src = [1, 2, 3, 4]
+    c = OmegaConf.create(src)
+
+    def resolve_with_default_throws(
+        key: Any, value: Any, default_value: Any = None,
+    ) -> None:
+        raise Exception("mocked_exception")
+
+    mocker.patch.object(c, "_resolve_with_default", resolve_with_default_throws)
+
+    with pytest.raises(Exception, match="mocked_exception"):
+        c.pop(0)
+    assert c == src
 
 
 def test_in_list() -> None:
@@ -119,7 +141,7 @@ def test_list_config_with_tuple() -> None:
 def test_items_on_list() -> None:
     c = OmegaConf.create([1, 2])
     with pytest.raises(AttributeError):
-        c.items()
+        c.items()  # type: ignore
 
 
 def test_list_enumerate() -> None:
@@ -142,15 +164,29 @@ def test_list_delitem() -> None:
     with pytest.raises(IndexError):
         del c[100]
 
+    validate_list_keys(c)
 
-def test_list_len() -> None:
-    c = OmegaConf.create([1, 2])
-    assert len(c) == 2
+
+@pytest.mark.parametrize(  # type: ignore
+    "lst,expected",
+    [
+        (OmegaConf.create([1, 2]), 2),
+        (ListConfig(content=None), 0),
+        (ListConfig(content="???"), 0),
+    ],
+)
+def test_list_len(lst: Any, expected: Any) -> None:
+    assert len(lst) == expected
 
 
 def test_nested_list_assign_illegal_value() -> None:
-    c = OmegaConf.create(dict(a=[None]))
-    with pytest.raises(UnsupportedValueType, match=re.escape("key: a[0]")):
+    c = OmegaConf.create({"a": [None]})
+    with pytest.raises(
+        UnsupportedValueType,
+        match=re.escape(
+            "Value 'IllegalType' is not a supported primitive type\n\tfull_key: a[0]"
+        ),
+    ):
         c.a[0] = IllegalType()
 
 
@@ -162,6 +198,8 @@ def test_list_append() -> None:
     c.append([])
     assert c == [1, 2, {}, []]
 
+    validate_list_keys(c)
+
 
 def test_pretty_without_resolve() -> None:
     c = OmegaConf.create([100, "${0}"])
@@ -194,31 +232,72 @@ def test_list_dir() -> None:
     assert ["0", "1", "2"] == dir(c)
 
 
-def test_getattr() -> None:
-    c = OmegaConf.create(["a", "b", "c"])
-    assert getattr(c, "0") == "a"
-    assert getattr(c, "1") == "b"
-    assert getattr(c, "2") == "c"
-    with pytest.raises(AttributeError):
-        getattr(c, "anything")
+def validate_list_keys(c: Any) -> None:
+    # validate keys are maintained
+    for i in range(len(c)):
+        assert c.get_node(i)._metadata.key == i
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "input_, index, value, expected, expected_node_type",
+    "input_, index, value, expected, expected_node_type, expectation",
     [
-        (["a", "b", "c"], 1, 100, ["a", 100, "b", "c"], AnyNode),
-        (["a", "b", "c"], 1, IntegerNode(100), ["a", 100, "b", "c"], IntegerNode),
-        (["a", "b", "c"], 1, "foo", ["a", "foo", "b", "c"], AnyNode),
-        (["a", "b", "c"], 1, StringNode("foo"), ["a", "foo", "b", "c"], StringNode),
+        (["a", "b", "c"], 1, 100, ["a", 100, "b", "c"], AnyNode, None),
+        (
+            ["a", "b", "c"],
+            1,
+            IntegerNode(100),
+            ["a", 100, "b", "c"],
+            IntegerNode,
+            None,
+        ),
+        (["a", "b", "c"], 1, "foo", ["a", "foo", "b", "c"], AnyNode, None),
+        (
+            ["a", "b", "c"],
+            1,
+            StringNode("foo"),
+            ["a", "foo", "b", "c"],
+            StringNode,
+            None,
+        ),
+        (
+            ListConfig(ref_type=List[int], content=[]),
+            0,
+            "foo",
+            None,
+            None,
+            ValidationError,
+        ),
     ],
 )
 def test_insert(
-    input_: List[str], index: int, value: Any, expected: Any, expected_node_type: type
+    input_: List[str],
+    index: int,
+    value: Any,
+    expected: Any,
+    expected_node_type: type,
+    expectation: Any,
 ) -> None:
     c = OmegaConf.create(input_)
-    c.insert(index, value)
-    assert c == expected
-    assert type(c.get_node(index)) == expected_node_type
+    if expectation is None:
+        c.insert(index, value)
+        assert c == expected
+        assert type(c.get_node(index)) == expected_node_type
+    else:
+        with pytest.raises(expectation):
+            c.insert(index, value)
+    validate_list_keys(c)
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "lst,idx,value,expectation",
+    [
+        (ListConfig(content=None), 0, 10, pytest.raises(TypeError)),
+        (ListConfig(content="???"), 0, 10, pytest.raises(MissingMandatoryValue)),
+    ],
+)
+def test_insert_special_list(lst: Any, idx: Any, value: Any, expectation: Any) -> None:
+    with expectation:
+        lst.insert(idx, value)
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -320,10 +399,12 @@ def test_insert_throws_not_changing_list() -> None:
 
 def test_append_throws_not_changing_list() -> None:
     c = OmegaConf.create([])
+    v = IllegalType()
     with pytest.raises(ValueError):
-        c.append(IllegalType())
+        c.append(v)
     assert len(c) == 0
     assert c == []
+    validate_list_keys(c)
 
 
 def test_hash() -> None:
@@ -372,3 +453,36 @@ def test_set_with_invalid_key() -> None:
     cfg = OmegaConf.create([1, 2, 3])
     with pytest.raises(KeyValidationError):
         cfg["foo"] = 4  # type: ignore
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "lst,idx,expected",
+    [
+        (OmegaConf.create([1, 2]), 0, 1),
+        (ListConfig(content=None), 0, TypeError),
+        (ListConfig(content="???"), 0, MissingMandatoryValue),
+    ],
+)
+def test_getitem(lst: Any, idx: Any, expected: Any) -> None:
+    if isinstance(expected, type):
+        with pytest.raises(expected):
+            lst.__getitem__(idx)
+    else:
+        lst.__getitem__(idx) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "lst,idx,expected",
+    [
+        (OmegaConf.create([1, 2]), 0, 1),
+        (OmegaConf.create([1, 2]), "foo", KeyValidationError),
+        (ListConfig(content=None), 0, TypeError),
+        (ListConfig(content="???"), 0, MissingMandatoryValue),
+    ],
+)
+def test_get(lst: Any, idx: Any, expected: Any) -> None:
+    if isinstance(expected, type):
+        with pytest.raises(expected):
+            lst.get(idx)
+    else:
+        lst.__getitem__(idx) == expected
diff --git a/tests/test_create.py b/tests/test_create.py
index 61a4dc9..d413c0e 100644
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -5,10 +5,10 @@ from typing import Any, Dict, List
 
 import pytest
 
-from omegaconf import OmegaConf
+from omegaconf import DictConfig, ListConfig, OmegaConf
 from omegaconf.errors import UnsupportedValueType
 
-from . import IllegalType
+from . import ConcretePlugin, IllegalType, Plugin
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -22,7 +22,7 @@ from . import IllegalType
         ("hello", {"hello": None}),
         # simple key:value"
         ("hello: world", {"hello": "world"}),
-        (dict(hello=dict(a=2)), {"hello": {"a": 2}}),
+        ({"hello": {"a": 2}}, {"hello": {"a": 2}}),
         # empty input
         ("", {}),
         # list value
@@ -38,6 +38,7 @@ from . import IllegalType
             {"a": 2, "b": {"c": {"f": 1}, "d": {}}},
             {"a": 2, "b": {"c": {"f": 1}, "d": {}}},
         ),
+        ({"a": None}, {"a": None}),
         ({"foo": "${missing}"}, {"foo": "${missing}"}),
         (OmegaConf.create({"foo": "${missing}"}), {"foo": "${missing}"}),
         (OmegaConf.create(), {}),
@@ -85,8 +86,9 @@ def test_create_list_with_illegal_value_idx0() -> None:
 
 
 def test_create_list_with_illegal_value_idx1() -> None:
+    lst = [1, IllegalType(), 3]
     with pytest.raises(UnsupportedValueType, match=re.escape("key: [1]")):
-        OmegaConf.create([1, IllegalType(), 3])
+        OmegaConf.create(lst)
 
 
 def test_create_dict_with_illegal_value() -> None:
@@ -112,3 +114,21 @@ def test_create_from_oc_with_flags() -> None:
     c2 = OmegaConf.create(c1)
     assert c1 == c2
     assert c1._metadata.flags == c2._metadata.flags
+
+
+def test_create_from_dictconfig_preserves_metadata() -> None:
+    cfg1 = DictConfig(ref_type=Plugin, is_optional=False, content=ConcretePlugin)
+    OmegaConf.set_struct(cfg1, True)
+    OmegaConf.set_readonly(cfg1, True)
+    cfg2 = OmegaConf.create(cfg1)
+    assert cfg1 == cfg2
+    assert cfg1._metadata == cfg2._metadata
+
+
+def test_create_from_listconfig_preserves_metadata() -> None:
+    cfg1 = ListConfig(ref_type=List[int], is_optional=False, content=[1, 2, 3])
+    OmegaConf.set_struct(cfg1, True)
+    OmegaConf.set_readonly(cfg1, True)
+    cfg2 = OmegaConf.create(cfg1)
+    assert cfg1 == cfg2
+    assert cfg1._metadata == cfg2._metadata
diff --git a/tests/test_errors.py b/tests/test_errors.py
new file mode 100644
index 0000000..0471de3
--- /dev/null
+++ b/tests/test_errors.py
@@ -0,0 +1,504 @@
+import re
+from dataclasses import dataclass
+from typing import Any, Dict, List
+
+import pytest
+
+from omegaconf import (
+    DictConfig,
+    IntegerNode,
+    ListConfig,
+    OmegaConf,
+    ReadonlyConfigError,
+    UnsupportedValueType,
+    ValidationError,
+)
+from omegaconf.errors import KeyValidationError, MissingMandatoryValue
+
+from . import Color, ConcretePlugin, IllegalType, Plugin, StructuredWithMissing
+
+
+@dataclass
+class NonOptionalAssignedNone:
+    foo: int = None  # type:ignore
+
+
+def create_readonly(cfg: Any) -> Any:
+    cfg = OmegaConf.create(cfg)
+    OmegaConf.set_readonly(cfg, True)
+    return cfg
+
+
+params = [
+    ##############
+    # DictConfig #
+    ##############
+    # update_node
+    pytest.param(
+        lambda: OmegaConf.structured(StructuredWithMissing),
+        lambda cfg: cfg.update_node("num", "hello"),
+        ValidationError,
+        "Value 'hello' could not be converted to Integer",
+        id="structured:update_node_with_invalid_value",
+    ),
+    pytest.param(
+        lambda: OmegaConf.structured(StructuredWithMissing),
+        lambda cfg: cfg.update_node("num", None),
+        ValidationError,
+        "field 'num' is not Optional",
+        id="structured:update_node:none_to_non_optional",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create({}),
+        lambda cfg: cfg.update_node("a", IllegalType()),
+        UnsupportedValueType,
+        "Value 'IllegalType' is not a supported primitive type",
+        id="dict:update_node:object_of_illegal_type",
+    ),
+    # pop
+    pytest.param(
+        lambda: create_readonly({"foo": "bar"}),
+        lambda cfg: cfg.pop("foo"),
+        ReadonlyConfigError,
+        "Cannot pop from read-only node",
+        id="dict,readonly:pop",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create({"foo": "bar"}),
+        lambda cfg: cfg.pop("nevermind"),
+        KeyError,
+        "Key not found: 'nevermind'",
+        id="dict:pop_invalid",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create({"foo": {}}),
+        lambda cfg: cfg.foo.pop("nevermind"),
+        KeyError,
+        "Key not found: 'nevermind' (path: 'foo.nevermind')",
+        id="dict:pop_invalid",
+    ),
+    pytest.param(
+        lambda: OmegaConf.structured(ConcretePlugin),
+        lambda cfg: getattr(cfg, "fail"),
+        AttributeError,
+        "Key 'fail' not in 'ConcretePlugin'",
+        id="structured:access_invalid_attribute",
+    ),
+    # getattr
+    pytest.param(
+        lambda: create_struct({"foo": "bar"}),
+        lambda cfg: getattr(cfg, "fail"),
+        AttributeError,
+        "Key 'fail' in not in struct",
+        id="dict,struct:access_invalid_attribute",
+    ),
+    # setattr
+    pytest.param(
+        lambda: create_struct({"foo": "bar"}),
+        lambda cfg: setattr(cfg, "zlonk", "zlank"),
+        AttributeError,
+        "Key 'zlonk' in not in struct",
+        id="dict,struct:set_invalid_attribute",
+    ),
+    pytest.param(
+        lambda: OmegaConf.structured(ConcretePlugin),
+        lambda cfg: setattr(cfg, "params", 10),
+        ValidationError,
+        "Invalid type assigned : int is not a subclass of FoobarParams. value: 10",
+        id="structured:setattr,invalid_type",
+    ),
+    pytest.param(
+        lambda: create_readonly({"foo": "bar"}),
+        lambda cfg: setattr(cfg, "foo", 20),
+        ReadonlyConfigError,
+        "Cannot assign to read-only node : 20",
+        id="dict,readonly:set_attribute",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create({"foo": DictConfig(is_optional=False, content={})}),
+        lambda cfg: setattr(cfg, "foo", None),
+        ValidationError,
+        "field 'foo' is not Optional",
+        id="dict,none_optional:set_none",
+    ),
+    # setitem
+    pytest.param(
+        lambda: create_struct({"foo": "bar"}),
+        lambda cfg: cfg.__setitem__("zoo", "zonk"),
+        KeyError,
+        "Error setting zoo=zonk : Key 'zoo' in not in struct",
+        id="dict,struct:setitem_on_none_existing_key",
+    ),
+    # getitem
+    pytest.param(
+        lambda: create_struct({"foo": "bar"}),
+        lambda cfg: cfg.__getitem__("zoo"),
+        KeyError,
+        "Error getting 'zoo' : Key 'zoo' in not in struct",
+        id="dict,struct:getitem_key_not_in_struct",
+    ),
+    pytest.param(
+        lambda: DictConfig(ref_type=Dict[Color, str], content={}),
+        lambda cfg: cfg.__getitem__("foo"),
+        KeyValidationError,
+        "Key 'foo' is incompatible with (Color)",
+        id="dict,reftype=Dict[Color,str]:,getitem_str_key",
+    ),
+    pytest.param(
+        lambda: DictConfig(ref_type=Dict[str, str], content={}),
+        lambda cfg: cfg.__getitem__(Color.RED),
+        KeyValidationError,
+        "Key Color.RED (Color) is incompatible with (str)",
+        id="dict,reftype=Dict[str,str]:,getitem_color_key",
+    ),
+    # merge
+    pytest.param(
+        lambda: create_readonly({"foo": "bar"}),
+        lambda cfg: cfg.merge_with(OmegaConf.create()),
+        ReadonlyConfigError,
+        "Cannot merge into read-only node",
+        id="dict,readonly:merge_with",
+    ),
+    # merge_with
+    pytest.param(
+        lambda: OmegaConf.structured(ConcretePlugin),
+        lambda cfg: cfg.merge_with(Plugin),
+        ValidationError,
+        "Plugin is not a subclass of ConcretePlugin. value: {'name': '???', 'params': '???'}",
+        id="structured:merge_invalid_dataclass",
+    ),
+    # get
+    pytest.param(
+        lambda: OmegaConf.create(),
+        lambda cfg: cfg.get(IllegalType),
+        KeyValidationError,
+        "Incompatible key type 'type'",
+        id="dict:get_illegal_type",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create(),
+        lambda cfg: cfg.get(IllegalType()),
+        KeyValidationError,
+        "Incompatible key type 'IllegalType'",
+        id="dict:get_object_of_illegal_type",
+    ),
+    # create
+    pytest.param(
+        lambda: None,
+        lambda cfg: OmegaConf.structured(NonOptionalAssignedNone),
+        ValidationError,
+        "Non optional field cannot be assigned None",
+        id="dict_create_none_optional_with_none",
+    ),
+    pytest.param(
+        lambda: None,
+        lambda cfg: OmegaConf.structured(IllegalType),
+        ValidationError,
+        "Input class 'IllegalType' is not a structured config. did you forget to decorate it as a dataclass?",
+        id="dict_create_from_illegal_type",
+    ),
+    pytest.param(
+        lambda: None,
+        lambda cfg: OmegaConf.structured(IllegalType()),
+        ValidationError,
+        "Object of unsupported type: 'IllegalType'",
+        id="structured:create_from_unsupported_object",
+    ),
+    # assign
+    pytest.param(
+        lambda: DictConfig(ref_type=ConcretePlugin, content="???"),
+        lambda cfg: cfg._set_value(1),
+        ValidationError,
+        "Invalid type assigned : int is not a subclass of ConcretePlugin. value: 1",
+        id="dict:set_value:reftype_mismatch",
+    ),
+    pytest.param(
+        lambda: DictConfig(ref_type=Dict[str, int], content={"foo": 10, "bar": 20}),
+        lambda cfg: cfg.__setitem__("baz", "fail"),
+        ValidationError,
+        "Value 'fail' could not be converted to Integer",
+        id="dict,int_element_type:assigned_str_value",
+    ),
+    # delete
+    pytest.param(
+        lambda: create_readonly({"foo": "bar"}),
+        lambda cfg: cfg.__delitem__("foo"),
+        ReadonlyConfigError,
+        "Cannot delete item from read-only DictConfig",
+        id="dict,readonly:del",
+    ),
+    ##############
+    # ListConfig #
+    ##############
+    # get node
+    pytest.param(
+        lambda: OmegaConf.create([1, 2, 3]),
+        lambda cfg: cfg.get_node_ex("foo"),
+        TypeError,
+        "list indices must be integers or slices, not str",
+        id="list:get_nox_ex:invalid_index_type",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([1, 2, 3]),
+        lambda cfg: cfg.get_node_ex(20),
+        IndexError,
+        "list index out of range",
+        id="list:get_node_ex:index_out_of_range",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.get_node_ex(20),
+        TypeError,
+        "Cannot get_node from a ListConfig object representing None",
+        id="list:get_node_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: cfg.get_node_ex(20),
+        MissingMandatoryValue,
+        "Cannot get_node from a missing ListConfig",
+        id="list:get_node_missing",
+    ),
+    # create
+    pytest.param(
+        lambda: None,
+        lambda cfg: ListConfig(is_optional=False, content=None),
+        ValidationError,
+        "Non optional ListConfig cannot be constructed from None",
+        id="list:create_not_optional_with_none",
+    ),
+    # append
+    pytest.param(
+        lambda: OmegaConf.create([]),
+        lambda cfg: cfg.append(IllegalType()),
+        UnsupportedValueType,
+        "Value 'IllegalType' is not a supported primitive type",
+        id="list:append_value_of_illegal_type",
+    ),
+    # pop
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.pop(0),
+        ReadonlyConfigError,
+        "Cannot pop from read-only ListConfig",
+        id="dict:readonly:pop",
+    ),
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.pop("Invalid key type"),
+        ReadonlyConfigError,
+        "Cannot pop from read-only ListConfig",
+        id="dict:readonly:pop",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.pop(0),
+        TypeError,
+        "Cannot pop from a ListConfig object representing None",
+        id="list:pop_from_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: cfg.pop(0),
+        MissingMandatoryValue,
+        "Cannot pop from a missing ListConfig",
+        id="list:pop_from_missing",
+    ),
+    # getitem
+    pytest.param(
+        lambda: OmegaConf.create(["???"]),
+        lambda cfg: cfg.__getitem__(slice(0, 1)),
+        MissingMandatoryValue,
+        "Missing mandatory value: [slice(0, 1, None)]",
+        id="list:subscript_slice_with_missing",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([10, "???"]),
+        lambda cfg: cfg.__getitem__(1),
+        MissingMandatoryValue,
+        "Missing mandatory value: [1]",
+        id="list:subscript_index_with_missing",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([1, 2, 3]),
+        lambda cfg: cfg.__getitem__(20),
+        IndexError,
+        "list index out of range",
+        id="list:subscript:index_out_of_range",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([1, 2, 3]),
+        lambda cfg: cfg.__getitem__("foo"),
+        KeyValidationError,
+        "Invalid key type 'str'",
+        id="list:getitem,illegal_key_type",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.__getitem__(0),
+        TypeError,
+        "ListConfig object representing None is not subscriptable",
+        id="list:getitem,illegal_key_type",
+    ),
+    # setitem
+    pytest.param(
+        lambda: OmegaConf.create([None]),
+        lambda cfg: cfg.__setitem__(0, IllegalType()),
+        UnsupportedValueType,
+        "Value 'IllegalType' is not a supported primitive type",
+        id="list:setitem,illegal_value_type",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([1, 2, 3]),
+        lambda cfg: cfg.__setitem__("foo", 4),
+        KeyValidationError,
+        "Invalid key type 'str'",
+        id="list:setitem,illegal_key_type",
+    ),
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.__setitem__(0, 4),
+        ReadonlyConfigError,
+        "ListConfig is read-only",
+        id="list,readonly:setitem",
+    ),
+    # assign
+    pytest.param(
+        lambda: ListConfig(ref_type=List[int], content=[1, 2, 3]),
+        lambda cfg: cfg.__setitem__(0, "foo"),
+        ValidationError,
+        "Value 'foo' could not be converted to Integer",
+        id="list,int_elements:assigned_str_element",
+    ),
+    pytest.param(
+        # make sure OmegaConf.create is not losing critical metadata.
+        lambda: OmegaConf.create(ListConfig(ref_type=List[int], content=[1, 2, 3])),
+        lambda cfg: cfg.__setitem__(0, "foo"),
+        ValidationError,
+        "Value 'foo' could not be converted to Integer",
+        id="list,int_elements:assigned_str_element",
+    ),
+    pytest.param(
+        lambda: OmegaConf.create([IntegerNode(is_optional=False, value=0), 2, 3]),
+        lambda cfg: cfg.__setitem__(0, None),
+        ValidationError,
+        "[0] is not optional and cannot be assigned None",
+        id="list,not_optional:assigned_none",
+    ),
+    # index
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.index(99),
+        ValueError,
+        "Item not found in ListConfig",
+        id="list,readonly:index_not_found",
+    ),
+    # insert
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.insert(1, 99),
+        ReadonlyConfigError,
+        "Cannot insert into a read-only ListConfig",
+        id="list,readonly:insert",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.insert(1, 99),
+        TypeError,
+        "Cannot insert into ListConfig object representing None",
+        id="list:insert_into_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: cfg.insert(1, 99),
+        MissingMandatoryValue,
+        "Cannot insert into missing ListConfig",
+        id="list:insert_into_missing",
+    ),
+    # get
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.get(0),
+        TypeError,
+        "Cannot get from a ListConfig object representing None",
+        id="list:get_from_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: cfg.get(0),
+        MissingMandatoryValue,
+        "Cannot get from a missing ListConfig",
+        id="list:get_from_missing",
+    ),
+    # sort
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.sort(),
+        ReadonlyConfigError,
+        "Cannot sort a read-only ListConfig",
+        id="list:readonly:sort",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: cfg.sort(),
+        TypeError,
+        "Cannot sort a ListConfig object representing None",
+        id="list:sort_from_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: cfg.sort(),
+        MissingMandatoryValue,
+        "Cannot sort a missing ListConfig",
+        id="list:sort_from_missing",
+    ),
+    # iter
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.sort(),
+        ReadonlyConfigError,
+        "Cannot sort a read-only ListConfig",
+        id="list:readonly:sort",
+    ),
+    pytest.param(
+        lambda: ListConfig(content=None),
+        lambda cfg: iter(cfg),
+        TypeError,
+        "Cannot iterate on ListConfig object representing None",
+        id="list:iter_none",
+    ),
+    pytest.param(
+        lambda: ListConfig(content="???"),
+        lambda cfg: iter(cfg),
+        MissingMandatoryValue,
+        "Cannot iterate on a missing ListConfig",
+        id="list:iter_missing",
+    ),
+    # delete
+    pytest.param(
+        lambda: create_readonly([1, 2, 3]),
+        lambda cfg: cfg.__delitem__(0),
+        ReadonlyConfigError,
+        "Cannot delete item from read-only ListConfig",
+        id="list,readonly:del",
+    ),
+]
+
+
+@pytest.mark.parametrize(  # type:ignore
+    "create, op, exception_type, msg", params
+)
+def test_errors(create: Any, op: Any, exception_type: Any, msg: str) -> None:
+    cfg = create()
+    with pytest.raises(exception_type, match=re.escape(msg)):
+        try:
+            op(cfg)
+        except Exception as e:
+            # helps in debugging
+            raise e
+
+
+def create_struct(cfg: Any) -> Any:
+    cfg = OmegaConf.create(cfg)
+    OmegaConf.set_struct(cfg, True)
+    return cfg
diff --git a/tests/test_get_full_key.py b/tests/test_get_full_key.py
index 6c0acf7..eef4e71 100644
--- a/tests/test_get_full_key.py
+++ b/tests/test_get_full_key.py
@@ -2,10 +2,10 @@ from typing import Any
 
 import pytest
 
-from omegaconf import OmegaConf
+from omegaconf import IntegerNode, OmegaConf
 
 
-@pytest.mark.parametrize(
+@pytest.mark.parametrize(  # type: ignore
     "cfg, select, key, expected",
     [
         ({}, "", "a", "a"),
@@ -67,10 +67,23 @@ from omegaconf import OmegaConf
         # special cases
         # parent_with_missing_item
         ({"x": "???", "a": 1, "b": {"c": 1}}, "b", "c", "b.c"),
+        ({"foo": IntegerNode(value=10)}, "", "foo", "foo"),
+        ({"foo": {"bar": IntegerNode(value=10)}}, "foo", "bar", "foo.bar"),
     ],
 )
-class TestGetFullKeyMatrix:
-    def test(self, cfg: Any, select: str, key: Any, expected: Any) -> None:
-        c = OmegaConf.create(cfg)
-        node = c.select(select)
-        assert node._get_full_key(key) == expected
+def test_get_full_key_from_config(
+    cfg: Any, select: str, key: Any, expected: Any
+) -> None:
+    c = OmegaConf.create(cfg)
+    node = c.select(select)
+    assert node._get_full_key(key) == expected
+
+
+def test_value_node_get_full_key() -> None:
+    cfg = OmegaConf.create({"foo": IntegerNode(value=10)})
+    assert cfg.get_node("foo")._get_full_key(None) == "foo"  # type: ignore
+
+    node = IntegerNode(value=10)
+    assert node._get_full_key(None) == ""
+    node = IntegerNode(key="foo", value=10)
+    assert node._get_full_key(None) == "foo"
diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py
index dc5be45..d4bf56b 100644
--- a/tests/test_interpolation.py
+++ b/tests/test_interpolation.py
@@ -1,5 +1,6 @@
 import os
 import random
+import re
 from typing import Any, Dict
 
 import pytest
@@ -140,7 +141,9 @@ def test_env_interpolation1() -> None:
 
 def test_env_interpolation_not_found() -> None:
     c = OmegaConf.create({"path": "/test/${env:foobar}"})
-    with pytest.raises(KeyError):
+    with pytest.raises(
+        ValidationError, match=re.escape("Environment variable 'foobar' not found")
+    ):
         c.path
 
 
diff --git a/tests/test_matrix.py b/tests/test_matrix.py
index 250aa8e..28e2699 100644
--- a/tests/test_matrix.py
+++ b/tests/test_matrix.py
@@ -1,4 +1,5 @@
 import copy
+import re
 from typing import Any
 
 import pytest
@@ -67,28 +68,33 @@ def verify(
             lambda value, is_optional, key=None: DictConfig(
                 is_optional=is_optional, content=value, key=key
             ),
-            [
-                {},
-                {"foo": "bar"},
-                OmegaConf.create({}),
-                OmegaConf.create({"foo": "bar"}),
-            ],
+            [{}, {"foo": "bar"}],
         ),
         # ListConfig
         (
             lambda value, is_optional, key=None: ListConfig(
                 is_optional=is_optional, content=value, key=key
             ),
-            [[], [1, 2, 3], OmegaConf.create([]), OmegaConf.create([1, 2, 3])],
+            [[], [1, 2, 3]],
         ),
         # dataclass
         (
             lambda value, is_optional, key=None: DictConfig(
-                annotated_type=Group, is_optional=is_optional, content=value, key=key
+                ref_type=Group, is_optional=is_optional, content=value, key=key
             ),
             [Group, Group()],
         ),
     ],
+    ids=(
+        "BooleanNode",
+        "FloatNode",
+        "IntegerNode",
+        "StringNode",
+        "EnumNode",
+        "DictConfig",
+        "ListConfig",
+        "dataclass",
+    ),
 )
 class TestNodeTypesMatrix:
     def test_none_assignment_and_merging_in_dict(
@@ -96,10 +102,12 @@ class TestNodeTypesMatrix:
     ) -> None:
         values = copy.deepcopy(values)
         for value in values:
-            data = {"node": node_type(value=value, is_optional=False)}
+            node = node_type(value=value, is_optional=False)
+            data = {"node": node}
             cfg = OmegaConf.create(obj=data)
             verify(cfg, "node", none=False, opt=False, missing=False, inter=False)
-            with pytest.raises(ValidationError):
+            msg = "field 'node' is not Optional"
+            with pytest.raises(ValidationError, match=re.escape(msg)):
                 cfg.node = None
 
             with pytest.raises(ValidationError):
@@ -220,7 +228,8 @@ class TestNodeTypesMatrix:
                 cfg,
                 "resolver",
                 none=False,
-                opt=False,
+                # Note, resolvers are always optional because the underlying function may return None
+                opt=True,
                 missing=False,
                 inter=True,
                 exp=resolver_output,
@@ -335,7 +344,7 @@ class TestNodeTypesMatrix:
                 cfg,
                 "int_resolver",
                 none=False,
-                opt=False,
+                opt=True,
                 missing=False,
                 inter=True,
                 exp=resolver_output,
diff --git a/tests/test_merge.py b/tests/test_merge.py
index dc35661..5d6eaee 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -2,8 +2,15 @@ from typing import Any, Tuple
 
 import pytest
 
-from omegaconf import MISSING, DictConfig, OmegaConf, ValidationError, _utils, nodes
-from omegaconf.errors import ReadonlyConfigError
+from omegaconf import (
+    MISSING,
+    DictConfig,
+    OmegaConf,
+    ReadonlyConfigError,
+    ValidationError,
+    nodes,
+)
+from omegaconf._utils import is_structured_config
 
 from . import ConcretePlugin, ConfWithMissingDict, Group, Plugin, User, Users
 
@@ -67,13 +74,22 @@ from . import ConcretePlugin, ConfWithMissingDict, Group, Plugin, User, Users
         ([ConfWithMissingDict, {"dict": {"foo": "bar"}}], {"dict": {"foo": "bar"}}),
         ([{}, ConfWithMissingDict], {"dict": "???"}),
         ([{"user": User}, {"user": Group}], pytest.raises(ValidationError)),
+        (
+            [{"user": DictConfig(ref_type=User, content=User)}, {"user": Group}],
+            pytest.raises(ValidationError),
+        ),
         ([Plugin, ConcretePlugin], ConcretePlugin),
+        pytest.param(
+            [{"user": "???"}, {"user": Group}],
+            {"user": Group},
+            id="merge_into_missing_node",
+        ),
     ],
 )
 def test_merge(inputs: Any, expected: Any) -> None:
     configs = [OmegaConf.create(c) for c in inputs]
 
-    if isinstance(expected, (dict, list)) or _utils.is_structured_config(expected):
+    if isinstance(expected, (dict, list)) or is_structured_config(expected):
         merged = OmegaConf.merge(*configs)
         assert merged == expected
         # test input configs are not changed.
@@ -110,7 +126,7 @@ def test_merge_no_eq_verify(
 
 def test_merge_with_1() -> None:
     a = OmegaConf.create()
-    b = OmegaConf.create(dict(a=1, b=2))
+    b = OmegaConf.create({"a": 1, "b": 2})
     a.merge_with(b)
     assert a == b
 
@@ -119,12 +135,7 @@ def test_merge_with_2() -> None:
     a = OmegaConf.create()
     assert isinstance(a, DictConfig)
     a.inner = {}
-    b = OmegaConf.create(
-        """
-    a : 1
-    b : 2
-    """
-    )
+    b = OmegaConf.create({"a": 1, "b": 2})
     a.inner.merge_with(b)  # type: ignore
     assert a.inner == b
 
@@ -150,7 +161,7 @@ def test_merge_list_list() -> None:
         ({}, [], TypeError),
         ([], {}, TypeError),
         ([1, 2, 3], None, ValueError),
-        (dict(a=10), None, ValueError),
+        ({"a": 10}, None, ValueError),
     ],
 )
 def test_merge_error(base: Any, merge: Any, exception: Any) -> None:
@@ -160,23 +171,29 @@ def test_merge_error(base: Any, merge: Any, exception: Any) -> None:
         OmegaConf.merge(base, merge)
 
 
-def test_into_readonly_dict() -> None:
-    cfg = OmegaConf.create({"foo": "bar"})
+@pytest.mark.parametrize(  # type: ignore
+    "c1, c2", [({"foo": "bar"}, {"zoo": "foo"}), ([1, 2, 3], [4, 5, 6])]
+)
+def test_with_readonly(c1: Any, c2: Any) -> None:
+    cfg = OmegaConf.create(c1)
     OmegaConf.set_readonly(cfg, True)
-    with pytest.raises(ReadonlyConfigError):
-        OmegaConf.merge(cfg, {"zoo": "foo"})
+    cfg2 = OmegaConf.merge(cfg, c2)
+    assert OmegaConf.is_readonly(cfg2)
 
 
-def test_into_readonly_list() -> None:
-    cfg = OmegaConf.create([1, 2, 3])
+@pytest.mark.parametrize(  # type: ignore
+    "c1, c2", [({"foo": "bar"}, {"zoo": "foo"}), ([1, 2, 3], [4, 5, 6])]
+)
+def test_into_readonly(c1: Any, c2: Any) -> None:
+    cfg = OmegaConf.create(c1)
     OmegaConf.set_readonly(cfg, True)
     with pytest.raises(ReadonlyConfigError):
-        OmegaConf.merge(cfg, [4, 5, 6])
+        cfg.merge_with(c2)
 
 
 def test_parent_maintained() -> None:
-    c1 = OmegaConf.create(dict(a=dict(b=10)))
-    c2 = OmegaConf.create(dict(aa=dict(bb=100)))
+    c1 = OmegaConf.create({"a": {"b": 10}})
+    c2 = OmegaConf.create({"aa": {"bb": 100}})
     c3 = OmegaConf.merge(c1, c2)
     assert isinstance(c1, DictConfig)
     assert isinstance(c2, DictConfig)
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index fd4cc51..d0aae03 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -419,7 +419,7 @@ def test_node_wrap_illegal_type() -> None:
 
     from omegaconf.omegaconf import _node_wrap
 
-    with pytest.raises(ValueError):
+    with pytest.raises(ValidationError):
         _node_wrap(
             type_=UserClass, value=UserClass(), is_optional=False, parent=None, key=None
         )
diff --git a/tests/test_omegaconf.py b/tests/test_omegaconf.py
index 2316d7b..e0813de 100644
--- a/tests/test_omegaconf.py
+++ b/tests/test_omegaconf.py
@@ -1,4 +1,4 @@
-from typing import Any, Type
+from typing import Any
 
 import pytest
 from pytest import raises
@@ -17,7 +17,7 @@ from omegaconf import (
 )
 from omegaconf.errors import UnsupportedInterpolationType
 
-from . import Color, ConcretePlugin, StructuredWithMissing, does_not_raise
+from . import Color, ConcretePlugin, Plugin, StructuredWithMissing, does_not_raise
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -86,28 +86,72 @@ def test_is_missing_resets() -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "cfg, is_conf, is_list, is_dict, type_",
+    "cfg, expected",
     [
-        (None, False, False, False, None),
-        ({}, False, False, False, None),
-        ("aa", False, False, False, None),
-        (10, False, False, False, None),
-        (True, False, False, False, None),
-        (bool, False, False, False, None),
-        (StringNode("foo"), False, False, False, None),
-        (OmegaConf.create({}), True, False, True, None),
-        (OmegaConf.create([]), True, True, False, None),
-        (OmegaConf.structured(ConcretePlugin), True, False, True, ConcretePlugin),
-        (OmegaConf.structured(ConcretePlugin()), True, False, True, ConcretePlugin),
+        (None, False),
+        ({}, False),
+        ([], False),
+        ("aa", False),
+        (10, False),
+        (True, False),
+        (bool, False),
+        (StringNode("foo"), False),
+        (ConcretePlugin, False),
+        (ConcretePlugin(), False),
+        (OmegaConf.create({}), True),
+        (OmegaConf.create([]), True),
+        (OmegaConf.structured(ConcretePlugin), True),
+        (OmegaConf.structured(ConcretePlugin()), True),
     ],
 )
-def test_is_config(
-    cfg: Any, is_conf: bool, is_list: bool, is_dict: bool, type_: Type[Any]
-) -> None:
-    assert OmegaConf.is_config(cfg) == is_conf
-    assert OmegaConf.is_list(cfg) == is_list
-    assert OmegaConf.is_dict(cfg) == is_dict
-    assert OmegaConf.get_type(cfg) == type_
+def test_is_config(cfg: Any, expected: bool) -> None:
+    assert OmegaConf.is_config(cfg) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, expected",
+    [
+        (None, False),
+        ({}, False),
+        ([], False),
+        ("aa", False),
+        (10, False),
+        (True, False),
+        (bool, False),
+        (StringNode("foo"), False),
+        (ConcretePlugin, False),
+        (ConcretePlugin(), False),
+        (OmegaConf.create({}), False),
+        (OmegaConf.create([]), True),
+        (OmegaConf.structured(ConcretePlugin), False),
+        (OmegaConf.structured(ConcretePlugin()), False),
+    ],
+)
+def test_is_list(cfg: Any, expected: bool) -> None:
+    assert OmegaConf.is_list(cfg) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, expected",
+    [
+        (None, False),
+        ({}, False),
+        ([], False),
+        ("aa", False),
+        (10, False),
+        (True, False),
+        (bool, False),
+        (StringNode("foo"), False),
+        (ConcretePlugin, False),
+        (ConcretePlugin(), False),
+        (OmegaConf.create({}), True),
+        (OmegaConf.create([]), False),
+        (OmegaConf.structured(ConcretePlugin), True),
+        (OmegaConf.structured(ConcretePlugin()), True),
+    ],
+)
+def test_is_dict(cfg: Any, expected: bool) -> None:
+    assert OmegaConf.is_dict(cfg) == expected
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -156,7 +200,7 @@ def test_is_config(
         ),
         (
             lambda is_optional, missing: DictConfig(
-                annotated_type=ConcretePlugin,
+                ref_type=ConcretePlugin,
                 content=ConcretePlugin() if not missing else "???",
                 is_optional=is_optional,
             )
@@ -206,7 +250,7 @@ def test_is_optional(fac: Any, is_optional: bool) -> None:
         ),
         (
             lambda none: DictConfig(
-                annotated_type=ConcretePlugin,
+                ref_type=ConcretePlugin,
                 content=ConcretePlugin() if not none else None,
                 is_optional=True,
             )
@@ -263,12 +307,22 @@ def test_is_none(fac: Any, is_none: bool) -> None:
         ),
         (
             lambda inter: DictConfig(
-                annotated_type=ConcretePlugin,
+                ref_type=ConcretePlugin,
                 content=ConcretePlugin() if inter is None else inter,
                 is_optional=True,
             )
         ),
     ],
+    ids=[
+        "StringNode",
+        "IntegerNode",
+        "FloatNode",
+        "BooleanNode",
+        "EnumNode",
+        "ListConfig",
+        "DictConfig",
+        "ConcretePlugin",
+    ],
 )
 def test_is_interpolation(fac):
     obj = fac(inter=None)
@@ -283,3 +337,52 @@ def test_is_interpolation(fac):
         assert OmegaConf.is_interpolation(obj)
         cfg = OmegaConf.create({"node": obj})
         assert OmegaConf.is_interpolation(cfg, "node")
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, type_",
+    [
+        ({"foo": 10}, int),
+        ({"foo": 10.0}, float),
+        ({"foo": True}, bool),
+        ({"foo": "bar"}, str),
+        ({"foo": None}, type(None)),
+        ({"foo": ConcretePlugin()}, ConcretePlugin),
+        ({"foo": ConcretePlugin}, ConcretePlugin),
+        ({"foo": {}}, dict),
+        ({"foo": OmegaConf.create()}, dict),
+        ({"foo": []}, list),
+        ({"foo": OmegaConf.create([])}, list),
+    ],
+)
+def test_get_type(cfg: Any, type_: Any) -> None:
+    cfg = OmegaConf.create(cfg)
+    assert OmegaConf.get_type(cfg, "foo") == type_
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, type_",
+    [
+        ({}, None),  # missing foo key
+        ({"foo": 10}, None),
+        ({"foo": 10.0}, None),
+        ({"foo": True}, None),
+        ({"foo": "bar"}, None),
+        ({"foo": None}, None),
+        ({"foo": IntegerNode(10)}, int),
+        ({"foo": FloatNode(10.0)}, float),
+        ({"foo": BooleanNode(True)}, bool),
+        ({"foo": StringNode("bar")}, str),
+        ({"foo": EnumNode(enum_type=Color, value=Color.RED)}, Color),
+        ({"foo": ConcretePlugin()}, ConcretePlugin),
+        ({"foo": ConcretePlugin}, ConcretePlugin),
+        ({"foo": DictConfig(ref_type=Plugin, content=ConcretePlugin)}, Plugin),
+        ({"foo": {}}, None),
+        ({"foo": OmegaConf.create()}, dict),
+        ({"foo": []}, None),
+        ({"foo": OmegaConf.create([])}, list),
+    ],
+)
+def test_get_ref_type(cfg: Any, type_: Any) -> None:
+    cfg = OmegaConf.create(cfg)
+    assert OmegaConf.get_ref_type(cfg, "foo") == type_
diff --git a/tests/test_readonly.py b/tests/test_readonly.py
index 49d4550..29b93de 100644
--- a/tests/test_readonly.py
+++ b/tests/test_readonly.py
@@ -151,5 +151,6 @@ def test_readonly_from_cli() -> None:
     assert isinstance(c, DictConfig)
     OmegaConf.set_readonly(c, True)
     cli = OmegaConf.from_dotlist(["foo.bar=[2]"])
-    with raises(ReadonlyConfigError, match="foo"):
-        OmegaConf.merge(c, cli)
+    cfg2 = OmegaConf.merge(c, cli)
+    assert OmegaConf.is_readonly(c)
+    assert OmegaConf.is_readonly(cfg2)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8f637fd..fd77ea1 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,74 +5,79 @@ from typing import Any, Dict, List
 import attr
 import pytest
 
-from omegaconf import DictConfig, OmegaConf, _utils
-from omegaconf.errors import ValidationError
+from omegaconf import DictConfig, ListConfig, OmegaConf, _utils
+from omegaconf.errors import KeyValidationError, ValidationError
 from omegaconf.nodes import StringNode
 
-from . import Color, does_not_raise
+from . import Color, IllegalType, does_not_raise
 
 
 @pytest.mark.parametrize(  # type: ignore
     "target_type, value, expectation",
     [
         # Any
-        (Any, "foo", does_not_raise()),
-        (Any, True, does_not_raise()),
-        (Any, 1, does_not_raise()),
-        (Any, 1.0, does_not_raise()),
-        (Any, Color.RED, does_not_raise()),
+        (Any, "foo", None),
+        (Any, True, None),
+        (Any, 1, None),
+        (Any, 1.0, None),
+        (Any, Color.RED, None),
         # int
-        (int, "foo", pytest.raises(ValidationError)),
-        (int, True, pytest.raises(ValidationError)),
-        (int, 1, does_not_raise()),
-        (int, 1.0, pytest.raises(ValidationError)),
-        (int, Color.RED, pytest.raises(ValidationError)),
+        (int, "foo", ValidationError),
+        (int, True, ValidationError),
+        (int, 1, None),
+        (int, 1.0, ValidationError),
+        (int, Color.RED, ValidationError),
         # float
-        (float, "foo", pytest.raises(ValidationError)),
-        (float, True, pytest.raises(ValidationError)),
-        (float, 1, does_not_raise()),
-        (float, 1.0, does_not_raise()),
-        (float, Color.RED, pytest.raises(ValidationError)),
+        (float, "foo", ValidationError),
+        (float, True, ValidationError),
+        (float, 1, None),
+        (float, 1.0, None),
+        (float, Color.RED, ValidationError),
         # bool
-        (bool, "foo", pytest.raises(ValidationError)),
-        (bool, True, does_not_raise()),
-        (bool, 1, does_not_raise()),
-        (bool, 0, does_not_raise()),
-        (bool, 1.0, pytest.raises(ValidationError)),
-        (bool, Color.RED, pytest.raises(ValidationError)),
-        (bool, "true", does_not_raise()),
-        (bool, "false", does_not_raise()),
-        (bool, "on", does_not_raise()),
-        (bool, "off", does_not_raise()),
+        (bool, "foo", ValidationError),
+        (bool, True, None),
+        (bool, 1, None),
+        (bool, 0, None),
+        (bool, 1.0, ValidationError),
+        (bool, Color.RED, ValidationError),
+        (bool, "true", None),
+        (bool, "false", None),
+        (bool, "on", None),
+        (bool, "off", None),
         # str
-        (str, "foo", does_not_raise()),
-        (str, True, does_not_raise()),
-        (str, 1, does_not_raise()),
-        (str, 1.0, does_not_raise()),
-        (str, Color.RED, does_not_raise()),
+        (str, "foo", None),
+        (str, True, None),
+        (str, 1, None),
+        (str, 1.0, None),
+        (str, Color.RED, None),
         # Color
-        (Color, "foo", pytest.raises(ValidationError)),
-        (Color, True, pytest.raises(ValidationError)),
-        (Color, 1, does_not_raise()),
-        (Color, 1.0, pytest.raises(ValidationError)),
-        (Color, Color.RED, does_not_raise()),
-        (Color, "RED", does_not_raise()),
-        (Color, "Color.RED", does_not_raise()),
+        (Color, "foo", ValidationError),
+        (Color, True, ValidationError),
+        (Color, 1, None),
+        (Color, 1.0, ValidationError),
+        (Color, Color.RED, None),
+        (Color, "RED", None),
+        (Color, "Color.RED", None),
         # bad type
-        (Exception, "nope", pytest.raises(ValueError)),
+        (IllegalType, "nope", ValidationError),
     ],
 )
 def test_maybe_wrap(target_type: type, value: Any, expectation: Any) -> None:
-    with expectation:
-        from omegaconf.omegaconf import _maybe_wrap
+    from omegaconf.omegaconf import _maybe_wrap
 
+    if expectation is None:
         _maybe_wrap(
-            annotated_type=target_type,
-            key=None,
-            value=value,
-            is_optional=False,
-            parent=None,
+            ref_type=target_type, key=None, value=value, is_optional=False, parent=None,
         )
+    else:
+        with pytest.raises(expectation):
+            _maybe_wrap(
+                ref_type=target_type,
+                key=None,
+                value=value,
+                is_optional=False,
+                parent=None,
+            )
 
 
 class _TestEnum(Enum):
@@ -122,9 +127,9 @@ class _TestUserClass:
     ],
 )
 def test_valid_value_annotation_type(type_: type, expected: bool) -> None:
-    from omegaconf._utils import _valid_value_annotation_type
+    from omegaconf._utils import valid_value_annotation_type
 
-    assert _valid_value_annotation_type(type_) == expected
+    assert valid_value_annotation_type(type_) == expected
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -206,8 +211,8 @@ def test_value_kind(value: Any, kind: _utils.ValueKind) -> None:
 def test_re_parent() -> None:
     def validate(cfg1: DictConfig) -> None:
         assert cfg1._get_parent() is None
-        assert cfg1.get_node("str")._get_parent() == cfg1
-        assert cfg1.get_node("list")._get_parent() == cfg1
+        assert cfg1.get_node("str")._get_parent() == cfg1  # type:ignore
+        assert cfg1.get_node("list")._get_parent() == cfg1  # type:ignore
         assert cfg1.list.get_node(0)._get_parent() == cfg1.list
 
     cfg = OmegaConf.create({})
@@ -217,8 +222,8 @@ def test_re_parent() -> None:
 
     validate(cfg)
 
-    cfg.get_node("str")._set_parent(None)
-    cfg.get_node("list")._set_parent(None)
+    cfg.get_node("str")._set_parent(None)  # type:ignore
+    cfg.get_node("list")._set_parent(None)  # type:ignore
     cfg.list.get_node(0)._set_parent(None)  # type: ignore
     # noinspection PyProtectedMember
     cfg._re_parent()
@@ -236,3 +241,56 @@ def test_get_class() -> None:
 
     with pytest.raises(ImportError):
         _utils._get_class("tests.examples.test_dataclass_example.not_found")
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "key_type,expected_key_type",
+    [
+        (int, KeyValidationError),
+        (str, str),
+        (Color, Color),
+        (Any, None),
+        (None, KeyValidationError),
+    ],
+)
+@pytest.mark.parametrize(  # type: ignore
+    "value_type,expected_value_type",
+    [(int, int), (str, str), (Color, Color), (Any, None), (None, type(None))],
+)
+def test_get_key_value_types(
+    key_type: Any, expected_key_type: Any, value_type: Any, expected_value_type: Any,
+) -> None:
+    dt = Dict[key_type, value_type]  # type: ignore
+    if expected_key_type is not None and issubclass(expected_key_type, Exception):
+        with pytest.raises(expected_key_type):
+            _utils.get_dict_key_value_types(dt)
+
+    else:
+        assert _utils.get_dict_key_value_types(dt) == (
+            expected_key_type,
+            expected_value_type,
+        )
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "type_, is_primitive",
+    [
+        (int, True),
+        (float, True),
+        (bool, True),
+        (str, True),
+        (type(None), True),
+        (Color, True),
+        (list, False),
+        (ListConfig, False),
+        (dict, False),
+        (DictConfig, False),
+    ],
+)
+def test_is_primitive_type(type_: Any, is_primitive: bool) -> None:
+    assert _utils.is_primitive_type(type_) == is_primitive
+
+
+def test_deprectated_is_primitive_type() -> None:
+    with pytest.deprecated_call():
+        _utils._is_primitive_type(int)