Repository: pydicom/pydicom

Issue:
Dataset.setdefault() doesn't add an element when None is used
```python
>>> foo = {}
>>> val = foo.setdefault('PatientName', None)
>>> val is None
True
>>> 'PatientName' in foo
True
>>> from pydicom import Dataset
>>> ds = Dataset()
>>> val = ds.setdefault('PatientName', None)
>>> val is None
True
>>> 'PatientName' in ds
False
```


Discussion:
pep8speaks: Hello @scaramallion! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:













* In the file [`pydicom/tests/test_tag.py`](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py):

> [Line 280:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L280): [E111](https://duckduckgo.com/?q=pep8%20E111) indentation is not a multiple of four
> [Line 280:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L280): [E117](https://duckduckgo.com/?q=pep8%20E117) over-indented
> [Line 305:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L305): [E111](https://duckduckgo.com/?q=pep8%20E111) indentation is not a multiple of four
> [Line 305:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L305): [E117](https://duckduckgo.com/?q=pep8%20E117) over-indented
> [Line 319:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L319): [E111](https://duckduckgo.com/?q=pep8%20E111) indentation is not a multiple of four
> [Line 319:14](https://github.com/pydicom/pydicom/blob/a4418ffe19b88aeb0a38b510294012c5ea3d1c7d/pydicom/tests/test_tag.py#L319): [E117](https://duckduckgo.com/?q=pep8%20E117) over-indented


codecov[bot]: # [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=h1) Report
> Merging [#1216](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=desc) into [master](https://codecov.io/gh/pydicom/pydicom/commit/a289cd2bc038afd720d63388f1d31b1b5b95e0c9?el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `96.61%`.

[![Impacted file tree graph](https://codecov.io/gh/pydicom/pydicom/pull/1216/graphs/tree.svg?width=650&height=150&src=pr&token=YciLV15Lwr)](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1216      +/-   ##
==========================================
- Coverage   94.85%   94.83%   -0.02%     
==========================================
  Files          55       55              
  Lines        9212     9223      +11     
==========================================
+ Hits         8738     8747       +9     
- Misses        474      476       +2     
```


| [Impacted Files](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pydicom/dataset.py](https://codecov.io/gh/pydicom/pydicom/pull/1216/diff?src=pr&el=tree#diff-cHlkaWNvbS9kYXRhc2V0LnB5) | `98.06% <95.91%> (-0.29%)` | :arrow_down: |
| [pydicom/config.py](https://codecov.io/gh/pydicom/pydicom/pull/1216/diff?src=pr&el=tree#diff-cHlkaWNvbS9jb25maWcucHk=) | `100.00% <100.00%> (ø)` | |
| [pydicom/dataelem.py](https://codecov.io/gh/pydicom/pydicom/pull/1216/diff?src=pr&el=tree#diff-cHlkaWNvbS9kYXRhZWxlbS5weQ==) | `95.94% <100.00%> (+0.04%)` | :arrow_up: |
| [pydicom/tag.py](https://codecov.io/gh/pydicom/pydicom/pull/1216/diff?src=pr&el=tree#diff-cHlkaWNvbS90YWcucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=footer). Last update [a289cd2...924046b](https://codecov.io/gh/pydicom/pydicom/pull/1216?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/doc/reference/config.rst b/doc/reference/config.rst
index a8d4ee162..ad9d3ddfc 100644
--- a/doc/reference/config.rst
+++ b/doc/reference/config.rst
@@ -26,4 +26,5 @@ Configuration Options (:mod:`pydicom.config`)
    use_IS_numpy
    use_DS_numpy
    APPLY_J2K_CORRECTIONS
+   INVALID_KEY_BEHAVIOR
    INVALID_KEYWORD_BEHAVIOR
diff --git a/doc/release_notes/v2.1.0.rst b/doc/release_notes/v2.1.0.rst
index e6c65d70a..c764f3d84 100644
--- a/doc/release_notes/v2.1.0.rst
+++ b/doc/release_notes/v2.1.0.rst
@@ -46,6 +46,10 @@ Enhancements
   :attr:`~pydicom.config.INVALID_KEYWORD_BEHAVIOR` config option to allow
   customizing the behavior when a camel case variable name is used that isn't
   a known element keyword (:issue:`1014`)
+* Added :attr:`~pydicom.config.INVALID_KEY_BEHAVIOR` config option to allow
+  customizing the behavior when an invalid key is used with the
+  :class:`~pydicom.dataset.Dataset` :func:`in<operator.__contains__>` operator
+  (:issue:`1200`)
 * Implemented full support (loading, accessing, modifying, writing) of
   DICOM File-sets and their DICOMDIR files via the
   :class:`~pydicom.fileset.FileSet` class (:issue:`9`, :issue:`243`,
@@ -64,9 +68,6 @@ Changes
   become ``MultiFrame``, those with ``and`` in them become ``And``, and
   ``DICOSQuadrupoleResonanceQRStorage`` becomes
   ``DICOSQuadrupoleResonanceStorage``.
-* Invalid values used with the :class:`~pydicom.dataset.Dataset`
-  :func:`in<operator.__contains__>` now raise :class:`ValueError` rather than
-  returning ``False`` (:issue:`1200`)
 * Data dictionaries updated to version 2020c of the DICOM Standard
 * The following UID constants are deprecated and will be removed in v2.2:
 
@@ -112,3 +113,7 @@ Fixes
 * Fixed empty ambiguous VR elements raising an exception (:issue:`1193`)
 * Allow :func:`~pydicom.pixel_data_handlers.util.apply_voi_lut` to apply VOI
   lookup to an input float array
+* Fixed :meth:`Dataset.setdefault()<pydicom.dataset.Dataset.setdefault>` not
+  adding working correctly when the `default` value is ``None`` and not
+  adding private elements when :attr:`~pydicom.config.enforce_valid_values` is
+  ``True`` (:issue:`1215`)
diff --git a/pydicom/config.py b/pydicom/config.py
index 81fb4cc5d..2b9aff92e 100644
--- a/pydicom/config.py
+++ b/pydicom/config.py
@@ -313,7 +313,37 @@ Examples
 >>> ds.PatientsName = "Citizen^Jan"
 ../pydicom/dataset.py:1895: UserWarning: Camel case attribute 'PatientsName'
 used which is not in the element keyword data dictionary
+"""
+
+INVALID_KEY_BEHAVIOR = "WARN"
+"""Control the behavior when invalid keys are used with
+:meth:`~pydicom.dataset.Dataset.__contains__` (e.g. ``'invalid' in ds``).
+
+.. versionadded:: 2.1
+
+Invalid keys are objects that cannot be converted to a
+:class:`~pydicom.tag.BaseTag`, such as unknown element keywords or invalid
+element tags like ``0x100100010``.
+
+If ``"WARN"`` (default), then warn when an invalid key is used, if ``"RAISE"``
+then raise a :class:`ValueError` exception. If ``"IGNORE"`` then neither warn
+nor raise.
 
+Examples
+--------
+
+>>> from pydicom import config
+>>> config.INVALID_KEY_BEHAVIOR = "RAISE"
+>>> ds = Dataset()
+>>> 'PatientName' in ds  # OK
+False
+>>> 'PatientsName' in ds
+Traceback (most recent call last):
+  File "<stdin>", line 1, in <module>
+  File ".../pydicom/dataset.py", line 494, in __contains__
+    raise ValueError(msg) from exc
+ValueError: Invalid value used with the 'in' operator: must be an
+element tag as a 2-tuple or int, or an element keyword
 """
 
 
diff --git a/pydicom/dataelem.py b/pydicom/dataelem.py
index 1b5323b07..f9dd85031 100644
--- a/pydicom/dataelem.py
+++ b/pydicom/dataelem.py
@@ -626,6 +626,14 @@ class DataElement:
             name = ""
         return name
 
+    @property
+    def is_private(self):
+        """Return ``True`` if the element's tag is private.
+
+        .. versionadded:: 2.1
+        """
+        return self.tag.is_private
+
     @property
     def is_retired(self):
         """Return the element's retired status as :class:`bool`.
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
index 38c5f8829..40b9687db 100644
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -23,7 +23,7 @@ import json
 import os
 import os.path
 import re
-from typing import Generator, TYPE_CHECKING
+from typing import Generator, TYPE_CHECKING, Optional, Tuple, Union
 import warnings
 
 if TYPE_CHECKING:
@@ -39,9 +39,9 @@ from pydicom import datadict, jsonrep, config
 from pydicom._version import __version_info__
 from pydicom.charset import default_encoding, convert_encodings
 from pydicom.config import logger
-from pydicom.datadict import dictionary_VR
-from pydicom.datadict import (tag_for_keyword, keyword_for_tag,
-                              repeater_has_keyword)
+from pydicom.datadict import (
+    dictionary_VR, tag_for_keyword, keyword_for_tag, repeater_has_keyword
+)
 from pydicom.dataelem import DataElement, DataElement_from_raw, RawDataElement
 from pydicom.fileutil import path_from_pathlike
 from pydicom.pixel_data_handlers.util import (
@@ -453,7 +453,9 @@ class Dataset(dict):
             return self[tag]
         return None
 
-    def __contains__(self, name):
+    def __contains__(
+        self, name: Union[int, str, Tuple[int, int], BaseTag]
+    ) -> bool:
         """Simulate dict.__contains__() to handle DICOM keywords.
 
         Examples
@@ -477,16 +479,21 @@ class Dataset(dict):
         """
         try:
             return Tag(name) in self._dict
-        except OverflowError as exc:
-            raise OverflowError(
-                "Invalid element tag value: tags have a maximum value of "
-                "(0xFFFF, 0xFFFF)"
-            ) from exc
         except Exception as exc:
-            raise ValueError(
+            msg = (
                 "Invalid value used with the 'in' operator: must be an "
                 "element tag as a 2-tuple or int, or an element keyword"
-            ) from exc
+            )
+            if isinstance(exc, OverflowError):
+                msg = (
+                    "Invalid element tag value used with the 'in' operator: "
+                    "tags have a maximum value of (0xFFFF, 0xFFFF)"
+                )
+
+            if config.INVALID_KEY_BEHAVIOR == "WARN":
+                warnings.warn(msg)
+            elif config.INVALID_KEY_BEHAVIOR == "RAISE":
+                raise ValueError(msg) from exc
 
     def decode(self):
         """Apply character set decoding to the elements in the
@@ -853,9 +860,12 @@ class Dataset(dict):
         if isinstance(key, BaseTag):
             tag = key
         else:
-            tag = Tag(key)
-        data_elem = self._dict[tag]
+            try:
+                tag = Tag(key)
+            except Exception as exc:
+                raise KeyError(f"'{key}'") from exc
 
+        data_elem = self._dict[tag]
         if isinstance(data_elem, DataElement):
             if data_elem.VR == 'SQ' and data_elem.value:
                 # let a sequence know its parent dataset, as sequence items
@@ -1225,7 +1235,11 @@ class Dataset(dict):
     def popitem(self):
         return self._dict.popitem()
 
-    def setdefault(self, key, default=None):
+    def setdefault(
+        self,
+        key: Union[int, str, Tuple[int, int], BaseTag],
+        default: Optional[object] = None
+    ) -> DataElement:
         """Emulate :meth:`dict.setdefault` with support for tags and keywords.
 
         Examples
@@ -1244,57 +1258,53 @@ class Dataset(dict):
 
         Parameters
         ----------
-        key : int or str or 2-tuple
+        key : int, str or 2-tuple of int
 
             * If :class:`tuple` - the group and element number of the DICOM tag
             * If :class:`int` - the combined group/element number
             * If :class:`str` - the DICOM keyword of the tag
-
-        default : type, optional
-            The default value that is inserted and returned if no data
-            element exists for the given key. If it is not of type
-            :class:`~pydicom.dataelem.DataElement`, one will be
-            constructed instead for the given tag and `default` as value.
-            This is only possible for known tags (e.g. tags found via the
-            dictionary lookup).
+        default : pydicom.dataelem.DataElement or object, optional
+            The :class:`~pydicom.dataelem.DataElement` to use with `key`, or
+            the value of the :class:`~pydicom.dataelem.DataElement` to use with
+            `key` (default ``None``).
 
         Returns
         -------
-        DataElement or type
-            The data element for `key` if it exists, or the default value if
-            it is a :class:`~pydicom.dataelem.DataElement` or
-            ``None``, or a :class:`~pydicom.dataelem.DataElement`
-            constructed with `default` as value.
+        pydicom.dataelem.DataElement or object
+            The :class:`~pydicom.dataelem.DataElement` for `key`.
 
         Raises
         ------
+        ValueError
+            If `key` is not convertible to a valid tag or a known element
+            keyword.
         KeyError
-            If the `key` is not a valid tag or keyword.
-            If `key` is an unknown non-private tag and
-            `config.enforce_valid_values` is set.
-            If no tag exists for `key`, default is not a
-            :class:`~pydicom.dataelem.DataElement` and not
-            ``None``, and `key` is not a known DICOM tag.
+            If :attr:`~pydicom.config.enforce_valid_values` is ``True`` and
+            `key` is an unknown non-private tag.
         """
-        if key in self:
-            return self[key]
-        if default is not None:
-            if not isinstance(default, DataElement):
-                tag = Tag(key)
+        tag = Tag(key)
+        if tag in self:
+            return self[tag]
+
+        if not isinstance(default, DataElement):
+            if tag.is_private:
+                vr = 'UN'
+            else:
                 try:
-                    vr = datadict.dictionary_VR(tag)
+                    vr = dictionary_VR(tag)
                 except KeyError:
-                    msg = "Unknown DICOM tag ({:04x}, {:04x})".format(
-                        tag.group, tag.element)
                     if config.enforce_valid_values:
-                        msg += " can't look up VR"
-                        raise KeyError(msg)
+                        raise KeyError(f"Unknown DICOM tag {tag}")
                     else:
                         vr = 'UN'
-                        msg += " - setting VR to 'UN'"
-                        warnings.warn(msg)
-                default = DataElement(Tag(key), vr, default)
-            self[key] = default
+                        warnings.warn(
+                            f"Unknown DICOM tag {tag} - setting VR to 'UN'"
+                        )
+
+            default = DataElement(tag, vr, default)
+
+        self[key] = default
+
         return default
 
     def convert_pixel_data(self, handler_name=''):
@@ -1902,7 +1912,7 @@ class Dataset(dict):
                 )
                 if config.INVALID_KEYWORD_BEHAVIOR == "WARN":
                     warnings.warn(msg)
-                elif config.INVALID_KEYWORD_BEHAVIOR == "ERROR":
+                elif config.INVALID_KEYWORD_BEHAVIOR == "RAISE":
                     raise ValueError(msg)
 
             # name not in dicom dictionary - setting a non-dicom instance
@@ -1921,51 +1931,65 @@ class Dataset(dict):
 
         self.__dict__["file_meta"] = value
 
-    def __setitem__(self, key, value):
-        """Operator for Dataset[key] = value.
-
-        Check consistency, and deal with private tags.
+    def __setitem__(
+        self,
+        key: Union[int, str, Tuple[int, int], BaseTag],
+        elem: Union[DataElement, RawDataElement]
+    ) -> None:
+        """Operator for ``Dataset[key] = elem``.
 
         Parameters
         ----------
         key : int or Tuple[int, int] or str
-            The tag for the element to be added to the Dataset.
-        value : dataelem.DataElement or dataelem.RawDataElement
+            The tag for the element to be added to the :class:`Dataset`.
+        elem : dataelem.DataElement or dataelem.RawDataElement
             The element to add to the :class:`Dataset`.
 
         Raises
         ------
         NotImplementedError
-            If `key` is a ``slice``.
+            If `key` is a :class:`slice`.
         ValueError
-            If the `key` value doesn't match ``DataElement.tag``.
+            If the `key` value doesn't match the corresponding
+            :attr:`DataElement.tag<pydicom.dataelem.tag>`.
         """
         if isinstance(key, slice):
-            raise NotImplementedError('Slicing is not supported for setting '
-                                      'Dataset elements.')
+            raise NotImplementedError(
+                'Slicing is not supported when setting Dataset items'
+            )
 
-        # OK if is subclass, e.g. DeferredDataElement
-        if not isinstance(value, (DataElement, RawDataElement)):
-            raise TypeError("Dataset contents must be DataElement instances.")
-        if isinstance(value.tag, BaseTag):
-            tag = value.tag
+        try:
+            key = Tag(key)
+        except Exception as exc:
+            raise ValueError(
+                f"Unable to convert the key '{key}' to an element tag"
+            ) from exc
+
+        if not isinstance(elem, (DataElement, RawDataElement)):
+            raise TypeError("Dataset items must be 'DataElement' instances")
+
+        if isinstance(elem.tag, BaseTag):
+            elem_tag = elem.tag
         else:
-            tag = Tag(value.tag)
-        if key != tag:
-            raise ValueError("DataElement.tag must match the dictionary key")
+            elem_tag = Tag(elem.tag)
+
+        if key != elem_tag:
+            raise ValueError(
+                f"The key '{key}' doesn't match the 'DataElement' tag "
+                f"'{elem_tag}'"
+            )
 
-        data_element = value
-        if tag.is_private:
+        if elem_tag.is_private:
             # See PS 3.5-2008 section 7.8.1 (p. 44) for how blocks are reserved
-            logger.debug("Setting private tag %r" % tag)
-            private_block = tag.elem >> 8
-            private_creator_tag = Tag(tag.group, private_block)
-            if private_creator_tag in self and tag != private_creator_tag:
-                if data_element.is_raw:
-                    data_element = DataElement_from_raw(
-                        data_element, self._character_set)
-                data_element.private_creator = self[private_creator_tag].value
-        self._dict[tag] = data_element
+            logger.debug(f"Setting private tag {elem_tag}")
+            private_block = elem_tag.elem >> 8
+            private_creator_tag = Tag(elem_tag.group, private_block)
+            if private_creator_tag in self and elem_tag != private_creator_tag:
+                if elem.is_raw:
+                    elem = DataElement_from_raw(elem, self._character_set)
+                elem.private_creator = self[private_creator_tag].value
+
+        self._dict[elem_tag] = elem
 
     def _slice_dataset(self, start, stop, step):
         """Return the element tags in the Dataset that match the slice.
diff --git a/pydicom/tag.py b/pydicom/tag.py
index 8551d7e92..b868a6f23 100644
--- a/pydicom/tag.py
+++ b/pydicom/tag.py
@@ -85,12 +85,16 @@ def Tag(
         elif isinstance(arg[0], int):
             valid = isinstance(arg[1], int)
         if not valid:
-            raise ValueError("Both arguments for Tag must be the same type, "
-                             "either string or int.")
+            raise TypeError(
+                f"Unable to create an element tag from '{arg}': both "
+                "arguments must be the same type and str or int"
+            )
 
         if arg[0] > 0xFFFF or arg[1] > 0xFFFF:
-            raise OverflowError("Groups and elements of tags must each "
-                                "be <=2 byte integers")
+            raise OverflowError(
+                f"Unable to create an element tag from '{arg}': the group "
+                "and element values are limited to a maximum of 2-bytes each"
+            )
 
         long_value = (arg[0] << 16) | arg[1]
 
@@ -99,25 +103,35 @@ def Tag(
         try:
             long_value = int(arg, 16)
             if long_value > 0xFFFFFFFF:
-                raise OverflowError("Tags are limited to 32-bit length; "
-                                    "tag {0!r}"
-                                    .format(long_value))
+                raise OverflowError(
+                    f"Unable to create an element tag from '{long_value}': "
+                    "the combined group and element values  are limited to a "
+                    "maximum of 4-bytes"
+                )
         except ValueError:
             # Try a DICOM keyword
             from pydicom.datadict import tag_for_keyword
             long_value = tag_for_keyword(arg)
             if long_value is None:
-                raise ValueError("'{}' is not a valid int or DICOM keyword"
-                                 .format(arg))
+                raise ValueError(
+                    f"Unable to create an element tag from '{arg}': "
+                    "unknown DICOM element keyword or an invalid int"
+                )
     # Single int parameter
     else:
         long_value = arg
         if long_value > 0xFFFFFFFF:
-            raise OverflowError("Tags are limited to 32-bit length; tag {0!r}"
-                                .format(long_value))
+            raise OverflowError(
+                f"Unable to create an element tag from '{long_value}': the "
+                "combined group and element values are limited to a maximum "
+                "of 4-bytes"
+            )
 
     if long_value < 0:
-        raise ValueError("Tags must be positive.")
+        raise ValueError(
+            f"Unable to create an element tag from '{long_value}': tags must "
+            "be positive"
+        )
 
     return BaseTag(long_value)
 


Test patch:
diff --git a/pydicom/tests/test_dataelem.py b/pydicom/tests/test_dataelem.py
index e2125a984..c9237001f 100644
--- a/pydicom/tests/test_dataelem.py
+++ b/pydicom/tests/test_dataelem.py
@@ -519,6 +519,13 @@ class TestDataElement:
         assert 0 == elem.VM
         assert elem.value == []
 
+    def test_is_private(self):
+        """Test the is_private property."""
+        elem = DataElement(0x00090010, 'UN', None)
+        assert elem.is_private
+        elem = DataElement(0x00080010, 'UN', None)
+        assert not elem.is_private
+
 
 class TestRawDataElement:
 
diff --git a/pydicom/tests/test_dataset.py b/pydicom/tests/test_dataset.py
index 2c3fcd77c..5b2ffbd98 100644
--- a/pydicom/tests/test_dataset.py
+++ b/pydicom/tests/test_dataset.py
@@ -23,7 +23,7 @@ from pydicom.tag import Tag
 from pydicom.uid import (
     ImplicitVRLittleEndian,
     ExplicitVRBigEndian,
-    JPEGBaseline,
+    JPEGBaseline8Bit,
     PYDICOM_IMPLEMENTATION_UID
 )
 
@@ -176,17 +176,17 @@ class TestDataset:
         assert hasattr(ds, 'SomeVariableName')
         assert 42 == ds.SomeVariableName
 
-    def test_membership(self):
+    def test_membership(self, contains_warn):
         """Dataset: can test if item present by 'if <name> in dataset'."""
         assert 'TreatmentMachineName' in self.ds
         msg = (
             r"Invalid value used with the 'in' operator: must be "
             r"an element tag as a 2-tuple or int, or an element keyword"
         )
-        with pytest.raises(ValueError, match=msg):
-            'Dummyname' not in self.ds
+        with pytest.warns(UserWarning, match=msg):
+            assert 'Dummyname' not in self.ds
 
-    def test_contains(self):
+    def test_contains(self, contains_warn):
         """Dataset: can test if item present by 'if <tag> in dataset'."""
         self.ds.CommandGroupLength = 100  # (0000,0000)
         assert (0x300a, 0xb2) in self.ds
@@ -199,20 +199,35 @@ class TestDataset:
             r"Invalid value used with the 'in' operator: must "
             r"be an element tag as a 2-tuple or int, or an element keyword"
         )
-        with pytest.raises(ValueError, match=msg):
-            (-0x0010, 0x0010) in self.ds
+        with pytest.warns(UserWarning, match=msg):
+            assert (-0x0010, 0x0010) not in self.ds
 
         # Random non-existent property
-        with pytest.raises(ValueError, match=msg):
-            assert 'random name' in self.ds
+        with pytest.warns(UserWarning, match=msg):
+            assert 'random name' not in self.ds
 
         # Overflowing tag
         msg = (
-            r"Invalid element tag value: tags have a maximum value of "
-            r"\(0xFFFF, 0xFFFF\)"
+            r"Invalid element tag value used with the 'in' operator: "
+            r"tags have a maximum value of \(0xFFFF, 0xFFFF\)"
+        )
+        with pytest.warns(UserWarning, match=msg):
+            assert 0x100100010 not in self.ds
+
+    def test_contains_raises(self, contains_raise):
+        """Test raising exception for invalid key."""
+        msg = (
+            r"Invalid value used with the 'in' operator: must "
+            r"be an element tag as a 2-tuple or int, or an element keyword"
         )
-        with pytest.raises(OverflowError, match=msg):
-            0x100100010 in self.ds
+        with pytest.raises(ValueError, match=msg):
+            'invalid' in self.ds
+
+    def test_contains_ignore(self, contains_ignore):
+        """Test ignoring invalid keys."""
+        with pytest.warns(None) as record:
+            assert 'invalid' not in self.ds
+            assert len(record) == 0
 
     def test_clear(self):
         assert 1 == len(self.ds)
@@ -293,26 +308,50 @@ class TestDataset:
         elem = self.ds.setdefault((0x0010, 0x0010), "Test")
         assert 'Test' == elem.value
         assert 2 == len(self.ds)
-        with pytest.warns(UserWarning, match=r'\(0011, 0010\)'):
-            self.ds.setdefault((0x0011, 0x0010), "Test")
 
     def test_setdefault_keyword(self):
+        """Test setdefault()."""
+        assert 'WindowWidth' not in self.ds
+        elem = self.ds.setdefault('WindowWidth', None)
+        assert elem.value is None
+        assert self.ds.WindowWidth is None
+        elem = self.ds.setdefault('WindowWidth', 1024)
+        assert elem.value is None
+        assert self.ds.WindowWidth is None
+
+        assert 'TreatmentMachineName' in self.ds
+        assert 'unit001' == self.ds.TreatmentMachineName
         elem = self.ds.setdefault('TreatmentMachineName', 'foo')
         assert 'unit001' == elem.value
+
         elem = self.ds.setdefault(
             'PatientName', DataElement(0x00100010, 'PN', "Test")
         )
         assert 'Test' == elem.value
-        assert 2 == len(self.ds)
+        assert 3 == len(self.ds)
 
     def test_setdefault_invalid_keyword(self):
+        """Test setdefault() with an invalid keyword raises."""
         msg = (
-            r"Invalid value used with the 'in' operator: must "
-            r"be an element tag as a 2-tuple or int, or an element keyword"
+            r"Unable to create an element tag from 'FooBar': unknown "
+            r"DICOM element keyword"
         )
         with pytest.raises(ValueError, match=msg):
             self.ds.setdefault('FooBar', 'foo')
 
+    def test_setdefault_private(self):
+        """Test setdefault() with a private tag."""
+        elem = self.ds.setdefault(0x00090020, None)
+        assert elem.is_private
+        assert elem.value is None
+        assert 'UN' == elem.VR
+        assert 0x00090020 in self.ds
+        elem.VR = 'PN'
+        elem = self.ds.setdefault(0x00090020, 'test')
+        assert elem.is_private
+        assert elem.value is None
+        assert 'PN' == elem.VR
+
     def test_get_exists1(self):
         """Dataset: dataset.get() returns an existing item by name."""
         assert 'unit001' == self.ds.get('TreatmentMachineName', None)
@@ -1262,7 +1301,7 @@ class TestDataset:
         ds.is_little_endian = True
         ds.is_implicit_VR = False
         ds.file_meta = FileMetaDataset()
-        ds.file_meta.TransferSyntaxUID = JPEGBaseline
+        ds.file_meta.TransferSyntaxUID = JPEGBaseline8Bit
         ds.PixelData = b'\x00\x01\x02\x03\x04\x05\x06'
         ds['PixelData'].VR = 'OB'
         msg = (
@@ -1279,7 +1318,7 @@ class TestDataset:
         ds.is_little_endian = True
         ds.is_implicit_VR = False
         ds.file_meta = FileMetaDataset()
-        ds.file_meta.TransferSyntaxUID = JPEGBaseline
+        ds.file_meta.TransferSyntaxUID = JPEGBaseline8Bit
         ds.PixelData = encapsulate([b'\x00\x01\x02\x03\x04\x05\x06'])
         ds['PixelData'].VR = 'OB'
         ds.save_as(fp)
@@ -1291,7 +1330,7 @@ class TestDataset:
         ds.is_little_endian = True
         ds.is_implicit_VR = False
         ds.file_meta = FileMetaDataset()
-        ds.file_meta.TransferSyntaxUID = JPEGBaseline
+        ds.file_meta.TransferSyntaxUID = JPEGBaseline8Bit
         ds.save_as(fp)
 
     def test_save_as_no_file_meta(self):
@@ -1346,7 +1385,7 @@ class TestDataset:
         ds.is_little_endian = True
         ds.is_implicit_VR = False
         ds.file_meta = FileMetaDataset()
-        ds.file_meta.TransferSyntaxUID = JPEGBaseline
+        ds.file_meta.TransferSyntaxUID = JPEGBaseline8Bit
         ds.PixelData = encapsulate([b'\x00\x01\x02\x03\x04\x05\x06'])
         elem = ds['PixelData']
         elem.VR = 'OB'
@@ -1564,6 +1603,24 @@ class TestDataset:
         ds2.update(ds)
         assert 'TestC' == ds2.PatientName
 
+    def test_getitem_invalid_key_raises(self):
+        """Test that __getitem__ raises KeyError on invalid key."""
+        ds = Dataset()
+        with pytest.raises(KeyError, match=r"'invalid'"):
+            ds['invalid']
+
+    def test_setitem_invalid_key(self):
+        """Test that __setitem__ behavior."""
+        a = {}
+        with pytest.raises(KeyError, match=r"'invalid'"):
+            a['invalid']
+
+        ds = Dataset()
+        ds.PatientName = 'Citizen^Jan'
+        msg = r"Unable to convert the key 'invalid' to an element tag"
+        with pytest.raises(ValueError, match=msg):
+            ds['invalid'] = ds['PatientName']
+
 
 class TestDatasetElements:
     """Test valid assignments of data elements"""
@@ -1945,6 +2002,31 @@ class TestFileMeta:
         assert ds_copy.read_encoding == "utf-8"
 
 
+@pytest.fixture
+def contains_raise():
+    """Raise on invalid keys with Dataset.__contains__()"""
+    original = config.INVALID_KEY_BEHAVIOR
+    config.INVALID_KEY_BEHAVIOR = "RAISE"
+    yield
+    config.INVALID_KEY_BEHAVIOR = original
+
+
+@pytest.fixture
+def contains_ignore():
+    """Ignore invalid keys with Dataset.__contains__()"""
+    original = config.INVALID_KEY_BEHAVIOR
+    config.INVALID_KEY_BEHAVIOR = "IGNORE"
+    yield
+    config.INVALID_KEY_BEHAVIOR = original
+
+
+@pytest.fixture
+def contains_warn():
+    """Warn on invalid keys with Dataset.__contains__()"""
+    original = config.INVALID_KEY_BEHAVIOR
+    config.INVALID_KEY_BEHAVIOR = "WARN"
+    yield
+    config.INVALID_KEY_BEHAVIOR = original
 CAMEL_CASE = (
     [  # Shouldn't warn
         "Rows", "_Rows", "Rows_", "rows", "_rows", "__rows", "rows_", "ro_ws",
@@ -1968,7 +2050,7 @@ CAMEL_CASE = (
 def setattr_raise():
     """Raise on Dataset.__setattr__() close keyword matches."""
     original = config.INVALID_KEYWORD_BEHAVIOR
-    config.INVALID_KEYWORD_BEHAVIOR = "ERROR"
+    config.INVALID_KEYWORD_BEHAVIOR = "RAISE"
     yield
     config.INVALID_KEYWORD_BEHAVIOR = original
 
diff --git a/pydicom/tests/test_tag.py b/pydicom/tests/test_tag.py
index 22c76b474..6807e7bc8 100644
--- a/pydicom/tests/test_tag.py
+++ b/pydicom/tests/test_tag.py
@@ -275,10 +275,12 @@ class TestTag:
         pytest.raises(ValueError, Tag, ('0x0', '-0x1'))
         pytest.raises(ValueError, Tag, ('-0x1', '0x0'))
         # Can't have second parameter
-        pytest.raises(ValueError, Tag, (0x01, 0x02), 0x01)
-        pytest.raises(ValueError, Tag, (0x01, 0x02), '0x01')
-        pytest.raises(ValueError, Tag, ('0x01', '0x02'), '0x01')
-        pytest.raises(ValueError, Tag, ('0x01', '0x02'), 0x01)
+        msg = r"Unable to create an element tag from '\(\(1, 2\), 1\)'"
+        with pytest.raises(TypeError, match=msg):
+            Tag((0x01, 0x02), 0x01)
+        pytest.raises(TypeError, Tag, (0x01, 0x02), '0x01')
+        pytest.raises(TypeError, Tag, ('0x01', '0x02'), '0x01')
+        pytest.raises(TypeError, Tag, ('0x01', '0x02'), 0x01)
 
     def test_tag_single_list(self):
         """Test creating a Tag from a single list."""
@@ -295,7 +297,12 @@ class TestTag:
         pytest.raises(ValueError, Tag, ['0x10'])
 
         # Must be int or string
-        pytest.raises(ValueError, Tag, [1., 2.])
+        msg = (
+            r"Unable to create an element tag from '\[1.0, 2.0\]': both "
+            r"arguments must be the same type and str or int"
+        )
+        with pytest.raises(TypeError, match=msg):
+            Tag([1., 2.])
 
         # Must be 32-bit
         pytest.raises(OverflowError, Tag, [65536, 0])
@@ -307,10 +314,13 @@ class TestTag:
         pytest.raises(ValueError, Tag, ('0x0', '-0x1'))
         pytest.raises(ValueError, Tag, ('-0x1', '0x0'))
         # Can't have second parameter
-        pytest.raises(ValueError, Tag, [0x01, 0x02], 0x01)
-        pytest.raises(ValueError, Tag, [0x01, 0x02], '0x01')
-        pytest.raises(ValueError, Tag, ['0x01', '0x02'], '0x01')
-        pytest.raises(ValueError, Tag, ['0x01', '0x02'], 0x01)
+        msg = r"Unable to create an element tag from '\(\[1, 2\], 1\)'"
+        with pytest.raises(TypeError, match=msg):
+            Tag([0x01, 0x02], 0x01)
+
+        pytest.raises(TypeError, Tag, [0x01, 0x02], '0x01')
+        pytest.raises(TypeError, Tag, ['0x01', '0x02'], '0x01')
+        pytest.raises(TypeError, Tag, ['0x01', '0x02'], 0x01)
 
     def test_tag_single_str(self):
         """Test creating a Tag from a single str."""
@@ -342,8 +352,8 @@ class TestTag:
         pytest.raises(ValueError, Tag, '0', '-0x01')
         pytest.raises(ValueError, Tag, '-1', '-0x01')
         # Must both be str
-        pytest.raises(ValueError, Tag, '0x01', 0)
-        pytest.raises(ValueError, Tag, 0, '0x01')
+        pytest.raises(TypeError, Tag, '0x01', 0)
+        pytest.raises(TypeError, Tag, 0, '0x01')
 
     def test_tag_double_int(self):
         """Test creating a Tag from a two ints."""