Repository: pydicom/pydicom

Issue:
Inconsistencies in value testing for PersonName3
```python
from pydicom.dataset import Dataset

ds = Dataset()
ds.PatientName = None  # or ''
if ds.PatientName:
    print('Has a value')
else:
    print('Has no value')

if None:  # or ''
    print('Evaluates as True')
else:
    print('Evaluates as False')
```
Prints `Has a value` then `Evaluates as False`. Should print `Has no value` instead (encoded dataset will have a zero-length element).

Current master, python 3.6.

Discussion:
pep8speaks: Hello @mrbean-bremen! 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_multival.py`](https://github.com/pydicom/pydicom/blob/77a0df9eb679135f63d2f035f0f1b80ecc2d6fba/pydicom/tests/test_multival.py):

> [Line 139:54](https://github.com/pydicom/pydicom/blob/77a0df9eb679135f63d2f035f0f1b80ecc2d6fba/pydicom/tests/test_multival.py#L139): [E203](https://duckduckgo.com/?q=pep8%20E203) whitespace before ','
> [Line 139:55](https://github.com/pydicom/pydicom/blob/77a0df9eb679135f63d2f035f0f1b80ecc2d6fba/pydicom/tests/test_multival.py#L139): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','




mrbean-bremen: Note - I rebased with master and squashed it all together to get rid of all the merge conflicts and duplicate commits.
mrbean-bremen: I'm setting this to MRG for another review and to discuss what has to be done in the scope of this PR, and what can possibly be done later.
mrbean-bremen: Ok, I changed the behavior:
- the value is retained after setting it to `None`, `''` or `[]`
- an excpeption is setting a text value to `b''` - this is handled for some VRs by decoding it to a `str`, and I didn't change that
- I added a single config value for the empty value set after decoding, currently set to `None`, e.g. all empty values are decoded as `None`

@darcymason - I think this is closer to what you had in mind, though I'm not sure if this sufficient.
Please have a look!
scaramallion: Can you add a simple test to confirm that changing `config.empty_value` gives the expected result?
mrbean-bremen: > Can you add a simple test to confirm that changing config.empty_value gives the expected result?

Sure, I wanted to do this, but obviously forgot it.
mrbean-bremen: Ok, now only two options are available: decode all empty values as `None`, or decode text values as `''` and other values as `None` (set as default as suggested by @scaramallion). Setting a value manually still retains that value.

Patch:
diff --git a/doc/old/python2_support.rst b/doc/old/python2_support.rst
index 42e6aed30..d1be1eaf7 100644
--- a/doc/old/python2_support.rst
+++ b/doc/old/python2_support.rst
@@ -24,11 +24,11 @@ After some discussion on github, the proposed plan for pydicom and Python 2
 support is as follows
 
 * pydicom v1.3 (July 2019) - no changes to Python versions supported. Adds a
-  deprecation warning (to be deprecated in v1.5) when run under Python 2
-* pydicom v1.4 (planned for release ~December 2019) will support Python 2.7,
-  with deprecation warning as above.
-* pydicom v1.5 (planned for ~April 2020) will be Python 3.5+ only
-* pydicom v1.6 (no date set) will be Python 3.6+
+  deprecation warning (to be deprecated after next major release) when run under Python 2
+* pydicom v1.4 (planned for release ~December 2019) will support Python 2.7 and Python 3.5+,
+  with Python 2 deprecation warning for following major release.
+* pydicom v2.0 (planned for ~April 2020) will be Python 3.5+ only
+* pydicom v2.1 (no date set) will be Python 3.6+
 
 We may consider the possibility of backporting some fixes to pydicom v1.4 for
 very serious issues, if users make the case for it.  Generally speaking,
diff --git a/doc/reference/elem.dataelem.rst b/doc/reference/elem.dataelem.rst
index c208fc86a..c790e4b02 100644
--- a/doc/reference/elem.dataelem.rst
+++ b/doc/reference/elem.dataelem.rst
@@ -12,4 +12,5 @@ Representation of DICOM data elements
 
    DataElement
    DataElement_from_raw
+   isMultiValue
    RawDataElement
diff --git a/doc/release_notes/v1.4.0.rst b/doc/release_notes/v1.4.0.rst
index 4804559ee..092e2920e 100644
--- a/doc/release_notes/v1.4.0.rst
+++ b/doc/release_notes/v1.4.0.rst
@@ -12,4 +12,4 @@ Fixes
 * Fixed testing PN values for truthiness (:issue:`891`)
 * Fixed handling of data too large to written in explicit transfer syntax
 * Fixed handling of known tags with VR UN (:issue:`899`)
-
+* Fixed assigning of empty values to data elements (:issue:`896`)
diff --git a/pydicom/charset.py b/pydicom/charset.py
index bf20c5231..aaec9e1d7 100644
--- a/pydicom/charset.py
+++ b/pydicom/charset.py
@@ -729,7 +729,7 @@ def _handle_illegal_standalone_encodings(encodings, py_encodings):
     return py_encodings
 
 
-def decode(data_element, dicom_character_set):
+def decode_element(data_element, dicom_character_set):
     """Apply the DICOM character encoding to a data element
 
     Parameters
@@ -742,6 +742,8 @@ def decode(data_element, dicom_character_set):
         single value, a multiple value (code extension), or may also be ``''``
         or ``None``, in which case ``'ISO_IR 6'`` will be used.
     """
+    if data_element.is_empty:
+        return data_element.empty_value
     if not dicom_character_set:
         dicom_character_set = ['ISO_IR 6']
 
@@ -751,14 +753,14 @@ def decode(data_element, dicom_character_set):
     # PN is special case as may have 3 components with different chr sets
     if data_element.VR == "PN":
         if not in_py2:
-            if data_element.VM == 1:
+            if data_element.VM <= 1:
                 data_element.value = data_element.value.decode(encodings)
             else:
                 data_element.value = [
                     val.decode(encodings) for val in data_element.value
                 ]
         else:
-            if data_element.VM == 1:
+            if data_element.VM <= 1:
                 data_element.value = PersonNameUnicode(data_element.value,
                                                        encodings)
             else:
@@ -785,3 +787,27 @@ def decode(data_element, dicom_character_set):
                                                 TEXT_VR_DELIMS))
 
             data_element.value = output
+
+
+def decode(data_element, dicom_character_set):
+    """Apply the DICOM character encoding to a data element
+
+    .. deprecated:: 1.4
+       This function is deprecated, use :func:`decode_element` instead.
+
+    Parameters
+    ----------
+    data_element : dataelem.DataElement
+        The :class:`DataElement<pydicom.dataelem.DataElement>` instance
+        containing an encoded byte string value to decode.
+    dicom_character_set : str or list of str or None
+        The value of (0008,0005) *Specific Character Set*, which may be a
+        single value, a multiple value (code extension), or may also be ``''``
+        or ``None``, in which case ``'ISO_IR 6'`` will be used.
+    """
+    warnings.warn(
+        "'charset.decode()' is deprecated and will be removed in "
+        "v1.5, use 'charset.decode_element()' instead",
+        DeprecationWarning
+    )
+    return decode_element(data_element, dicom_character_set)
diff --git a/pydicom/config.py b/pydicom/config.py
index 8d15adeef..a847c1052 100644
--- a/pydicom/config.py
+++ b/pydicom/config.py
@@ -86,6 +86,13 @@ TM to :class:`datetime.date`, :class:`datetime.datetime` and
 Default ``False``
 """
 
+use_none_as_empty_text_VR_value = False
+""" If ``True``, the value of decoded empty data element is always ``None``.
+If ``False`` (the default), the value of an empty data element with
+a text VR is an empty string, for all other VRs it is also ``None``.
+Note that the default of this value will change to ``True`` in version 2.0.
+"""
+
 # Logging system and debug function to change logging level
 logger = logging.getLogger('pydicom')
 logger.addHandler(logging.NullHandler())
diff --git a/pydicom/datadict.py b/pydicom/datadict.py
index 7fbf3c427..536832e91 100644
--- a/pydicom/datadict.py
+++ b/pydicom/datadict.py
@@ -11,7 +11,7 @@ from pydicom._dicom_dict import DicomDictionary
 # those with tags like "(50xx, 0005)"
 from pydicom._dicom_dict import RepeatersDictionary
 from pydicom._private_dict import private_dictionaries
-import warnings
+
 
 # Generate mask dict for checking repeating groups etc.
 # Map a true bitwise mask to the DICOM mask with "x"'s in it.
@@ -449,30 +449,6 @@ def tag_for_keyword(keyword):
     return keyword_dict.get(keyword)
 
 
-def tag_for_name(name):
-    """Return the tag of the element corresponding to `name`.
-
-    Only performs the lookup for official DICOM elements.
-
-    **This function is deprecated and will be removed in v1.4.**
-
-    Parameters
-    ----------
-    name : str
-        The keyword for the element whose tag is being retrieved.
-
-    Returns
-    -------
-    int or None
-        If the element is in the DICOM data dictionary then returns the
-        corresponding element's tag, otherwise returns ``None``.
-    """
-    msg = "tag_for_name is deprecated.  Use tag_for_keyword instead"
-    warnings.warn(msg, DeprecationWarning)
-
-    return tag_for_keyword(name)
-
-
 def repeater_has_tag(tag):
     """Return ``True`` if `tag` is in the DICOM repeaters data dictionary.
 
diff --git a/pydicom/dataelem.py b/pydicom/dataelem.py
index b955d6cac..7ff85d375 100644
--- a/pydicom/dataelem.py
+++ b/pydicom/dataelem.py
@@ -30,17 +30,62 @@ from pydicom import jsonrep
 import pydicom.valuerep  # don't import DS directly as can be changed by config
 
 from pydicom.valuerep import PersonNameUnicode
+
 if not in_py2:
     from pydicom.valuerep import PersonName3 as PersonNameUnicode
-    PersonName = PersonNameUnicode
+
+PersonName = PersonNameUnicode
+
+BINARY_VR_VALUES = [
+    'US', 'SS', 'UL', 'SL', 'OW', 'OB', 'OL', 'UN',
+    'OB or OW', 'US or OW', 'US or SS or OW', 'FL', 'FD', 'OF', 'OD'
+]
+
+
+def empty_value_for_VR(VR, raw=False):
+    """Return the value for an empty element for `VR`.
+
+    The behavior of this property depends on the setting of
+    :attr:`config.use_none_as_empty_value`. If that is set to ``True``,
+    an empty value is always represented by ``None``, otherwise it depends
+    on `VR`. For text VRs (not including VRs that represent a number in
+    textual representation) an empty string is used as empty value
+    representation, for all other VRs, ``None``.
+    Note that this is used only if decoding the element - it is always
+    possible to set the value to another empty value representation,
+    which will be preserved during the element object lifetime.
+
+    Parameters
+    ----------
+    VR : str
+        The VR of the corresponding element.
+
+    raw : bool
+        If ``True``, returns the value for a :class:`RawDataElement`,
+        otherwise for a :class:`DataElement`
+
+    Returns
+    -------
+    str or bytes or None
+        The value a data element with `VR` is assigned on decoding
+        if it is empty.
+    """
+    if config.use_none_as_empty_text_VR_value:
+        return None
+    if VR in ('AE', 'AS', 'CS', 'DA', 'DT', 'LO', 'LT',
+              'PN', 'SH', 'ST', 'TM', 'UC', 'UI', 'UR', 'UT'):
+        return b'' if raw else ''
+    return None
 
 
 def isMultiValue(value):
-    """Return True if `value` is list-like (iterable), False otherwise.
+    """Return ``True`` if `value` is list-like (iterable).
+
+    .. deprecation:: 1.3
+       This function is deprecated, use :attr:`DataElement.VM` instead.
 
-    **This function is deprecated and will be removed in v1.4**
     """
-    msg = 'isMultiValue is deprecated.  Use DataElement.VM instead'
+    msg = 'isMultiValue is deprecated, use DataElement.VM instead'
     warnings.warn(msg, DeprecationWarning)
     if isinstance(value, compat.char_types):
         return False
@@ -70,6 +115,7 @@ class DataElement(object):
     While its possible to create a new :class:`DataElement` directly and add
     it to a :class:`~pydicom.dataset.Dataset`:
 
+    >>> from pydicom import Dataset
     >>> elem = DataElement(0x00100010, 'PN', 'CITIZEN^Joan')
     >>> ds = Dataset()
     >>> ds.add(elem)
@@ -81,6 +127,20 @@ class DataElement(object):
     >>> ds = Dataset()
     >>> ds.PatientName = 'CITIZEN^Joan'
 
+    Empty DataElement objects (e.g. with VM = 0) show an empty string as
+    value for text VRs and `None` for non-text (binary) VRs:
+
+    >>> ds = Dataset()
+    >>> ds.PatientName = None
+    >>> ds.PatientName
+    ''
+
+    >>> ds.BitsAllocated = None
+    >>> ds.BitsAllocated
+
+    >>> str(ds.BitsAllocated)
+    'None'
+
     Attributes
     ----------
     descripWidth : int
@@ -183,7 +243,7 @@ class DataElement(object):
             except KeyError:
                 pass
 
-        self.VR = VR  # Note!: you must set VR before setting value
+        self.VR = VR  # Note: you must set VR before setting value
         if already_converted:
             self._value = value
         else:
@@ -441,14 +501,41 @@ class DataElement(object):
     @property
     def VM(self):
         """Return the value multiplicity of the element as :class:`int`."""
-        if isinstance(self.value, compat.char_types):
-            return 1
+        if self.value is None:
+            return 0
+        if isinstance(self.value, (compat.char_types, PersonName)):
+            return 1 if self.value else 0
         try:
             iter(self.value)
         except TypeError:
             return 1
         return len(self.value)
 
+    @property
+    def is_empty(self):
+        """Return `True` if the element has no value."""
+        return self.VM == 0
+
+    @property
+    def empty_value(self):
+        """Return the value for an empty element.
+
+        See :func:`empty_value_for_VR` for more information.
+
+        Returns
+        -------
+        str or None
+            The value this data element is assigned on decoding if it is empty.
+        """
+        return empty_value_for_VR(self.VR)
+
+    def clear(self):
+        """Clears the value, e.g. sets it to the configured empty value.
+
+        See :func:`empty_value_for_VR`.
+        """
+        self._value = self.empty_value
+
     def _convert_value(self, val):
         """Convert `val` to an appropriate type and return the result.
 
@@ -491,7 +578,7 @@ class DataElement(object):
         elif self.VR == 'TM' and config.datetime_conversion:
             return pydicom.valuerep.TM(val)
         elif self.VR == "UI":
-            return UID(val if val else '')
+            return UID(val) if val is not None else None
         elif not in_py2 and self.VR == "PN":
             return PersonName(val)
         # Later may need this for PersonName as for UI,
@@ -536,7 +623,7 @@ class DataElement(object):
 
     def __str__(self):
         """Return :class:`str` representation of the element."""
-        repVal = self.repval
+        repVal = self.repval or ''
         if self.showVR:
             s = "%s %-*s %s: %s" % (str(self.tag), self.descripWidth,
                                     self.description()[:self.descripWidth],
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
index cccc00415..79352e4b5 100644
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -53,12 +53,6 @@ except ImportError:
     have_numpy = False
 
 
-class PropertyError(Exception):
-    """For AttributeErrors caught in a property, so do not go to __getattr__"""
-    #  http://docs.python.org/release/3.1.3/tutorial/errors.html#tut-userexceptions
-    pass
-
-
 class PrivateBlock(object):
     """Helper class for a private block in the :class:`Dataset`.
 
@@ -498,10 +492,10 @@ class Dataset(dict):
         dicom_character_set = self._character_set
 
         # Shortcut to the decode function in pydicom.charset
-        decode_data_element = pydicom.charset.decode
+        decode_data_element = pydicom.charset.decode_element
 
         # Callback for walk(), to decode the chr strings if necessary
-        # This simply calls the pydicom.charset.decode function
+        # This simply calls the pydicom.charset.decode_element function
         def decode_callback(ds, data_element):
             """Callback to decode `data_element`."""
             if data_element.VR == 'SQ':
@@ -861,7 +855,7 @@ class Dataset(dict):
             return data_elem
         elif isinstance(data_elem, tuple):
             # If a deferred read, then go get the value now
-            if data_elem.value is None:
+            if data_elem.value is None and data_elem.length != 0:
                 from pydicom.filereader import read_deferred_data_element
                 data_elem = read_deferred_data_element(
                     self.fileobj_type, self.filename, self.timestamp,
@@ -1481,7 +1475,7 @@ class Dataset(dict):
                                getattr(data_element, x))
                               for x in dir(data_element)
                               if not x.startswith("_")
-                              and x not in ('from_json', 'to_json')])
+                              and x not in ('from_json', 'to_json', 'clear')])
             if data_element.VR == "SQ":
                 yield sequence_element_format % elem_dict
             else:
diff --git a/pydicom/filereader.py b/pydicom/filereader.py
index 7c222fabe..06bc2bee9 100644
--- a/pydicom/filereader.py
+++ b/pydicom/filereader.py
@@ -17,7 +17,7 @@ from pydicom.compat import in_py2
 from pydicom.config import logger
 from pydicom.datadict import dictionary_VR, tag_for_keyword
 from pydicom.dataelem import (DataElement, RawDataElement,
-                              DataElement_from_raw)
+                              DataElement_from_raw, empty_value_for_VR)
 from pydicom.dataset import (Dataset, FileDataset)
 from pydicom.dicomdir import DicomDir
 from pydicom.errors import InvalidDicomError
@@ -187,16 +187,14 @@ def data_element_generator(fp,
                              "Skipping forward to next data element.")
                 fp.seek(fp_tell() + length)
             else:
-                value = fp_read(length)
+                value = (fp_read(length) if length > 0
+                         else empty_value_for_VR(VR, raw=True))
                 if debugging:
-                    dotdot = "   "
-                    if length > 12:
-                        dotdot = "..."
-                    logger_debug("%08x: %-34s %s %r %s" % (value_tell,
-                                                           bytes2hex(
-                                                               value[:12]),
-                                                           dotdot,
-                                                           value[:12], dotdot))
+                    dotdot = "..." if length > 12 else "   "
+                    displayed_value = value[:12] if value else b''
+                    logger_debug("%08x: %-34s %s %r %s" %
+                                 (value_tell, bytes2hex(displayed_value),
+                                  dotdot, displayed_value, dotdot))
 
             # If the tag is (0008,0005) Specific Character Set, then store it
             if tag == BaseTag(0x00080005):
diff --git a/pydicom/filewriter.py b/pydicom/filewriter.py
index 1b00c5c05..a8c7982c3 100644
--- a/pydicom/filewriter.py
+++ b/pydicom/filewriter.py
@@ -279,7 +279,7 @@ def write_PN(fp, data_element, encodings=None):
     else:
         val = data_element.value
 
-    if isinstance(val[0], compat.text_type) or not in_py2:
+    if val and isinstance(val[0], compat.text_type) or not in_py2:
         try:
             val = [elem.encode(encodings) for elem in val]
         except TypeError:
@@ -472,15 +472,16 @@ def write_data_element(fp, data_element, encodings=None):
         encodings = convert_encodings(encodings)
         writer_function, writer_param = writers[VR]
         is_undefined_length = data_element.is_undefined_length
-        if VR in text_VRs or VR in ('PN', 'SQ'):
-            writer_function(buffer, data_element, encodings=encodings)
-        else:
-            # Many numeric types use the same writer but with numeric format
-            # parameter
-            if writer_param is not None:
-                writer_function(buffer, data_element, writer_param)
+        if not data_element.is_empty:
+            if VR in text_VRs or VR in ('PN', 'SQ'):
+                writer_function(buffer, data_element, encodings=encodings)
             else:
-                writer_function(buffer, data_element)
+                # Many numeric types use the same writer but with
+                # numeric format parameter
+                if writer_param is not None:
+                    writer_function(buffer, data_element, writer_param)
+                else:
+                    writer_function(buffer, data_element)
 
     # valid pixel data with undefined length shall contain encapsulated
     # data, e.g. sequence items - raise ValueError otherwise (see #238)
diff --git a/pydicom/valuerep.py b/pydicom/valuerep.py
index 292dca328..00d483f34 100644
--- a/pydicom/valuerep.py
+++ b/pydicom/valuerep.py
@@ -455,7 +455,7 @@ def DS(val):
     if isinstance(val, (str, compat.text_type)):
         val = val.strip()
     if val == '' or val is None:
-        return ''
+        return val
     return DSclass(val)
 
 
@@ -483,7 +483,7 @@ class IS(int):
     def __new__(cls, val):
         """Create instance if new integer string"""
         if val is None:
-            return ''
+            return val
         if isinstance(val, (str, compat.text_type)) and val.strip() == '':
             return ''
         # Overflow error in Python 2 for integers too large
@@ -624,6 +624,12 @@ def _encode_personname(components, encodings):
 
 
 class PersonName3(object):
+    def __new__(cls, *args, **kwargs):
+        # Handle None value by returning None instead of a PersonName3 object
+        if len(args) and args[0] is None:
+            return None
+        return super(PersonName3, cls).__new__(cls)
+
     def __init__(self, val, encodings=None, original_string=None):
         if isinstance(val, PersonName3):
             encodings = val.encodings
@@ -762,6 +768,10 @@ class PersonName3(object):
         # the encoding was unknown or incorrect - create a new
         # PersonName object with the changed encoding
         encodings = _verify_encodings(encodings)
+        if self.original_string is None:
+            # if the original encoding was not set, we set it now
+            self.original_string = _encode_personname(
+                self.components, self.encodings or [default_encoding])
         return PersonName3(self.original_string, encodings)
 
     def encode(self, encodings=None):
diff --git a/pydicom/values.py b/pydicom/values.py
index f9ea84679..5fceb0c8c 100644
--- a/pydicom/values.py
+++ b/pydicom/values.py
@@ -12,6 +12,7 @@ from pydicom import compat
 from pydicom.compat import in_py2
 from pydicom.charset import (default_encoding, text_VRs, decode_string)
 from pydicom.config import logger
+from pydicom.dataelem import empty_value_for_VR
 from pydicom.filereader import read_sequence
 from pydicom.multival import MultiValue
 from pydicom.tag import (Tag, TupleTag)
@@ -563,6 +564,9 @@ def convert_value(VR, raw_data_element, encodings=None):
         message = "Unknown Value Representation '{}'".format(VR)
         raise NotImplementedError(message)
 
+    if raw_data_element.length == 0:
+        return empty_value_for_VR(VR)
+
     # Look up the function to convert that VR
     # Dispatch two cases: a plain converter,
     # or a number one which needs a format string


Test patch:
diff --git a/pydicom/tests/test_charset.py b/pydicom/tests/test_charset.py
index 59620556e..0cc341e45 100644
--- a/pydicom/tests/test_charset.py
+++ b/pydicom/tests/test_charset.py
@@ -134,12 +134,12 @@ class TestCharset(object):
         """Test bad charset defaults to ISO IR 6"""
         # Python 3: elem.value is PersonName3, Python 2: elem.value is str
         elem = DataElement(0x00100010, 'PN', 'CITIZEN')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 126'])
+        pydicom.charset.decode_element(elem, ['ISO 2022 IR 126'])
         # After decode Python 2: elem.value is PersonNameUnicode
         assert 'iso_ir_126' in elem.value.encodings
         assert 'iso8859' not in elem.value.encodings
         # default encoding is iso8859
-        pydicom.charset.decode(elem, [])
+        pydicom.charset.decode_element(elem, [])
         assert 'iso8859' in elem.value.encodings
 
     def test_bad_encoded_single_encoding(self):
@@ -149,7 +149,7 @@ class TestCharset(object):
 
         with pytest.warns(UserWarning, match="Failed to decode byte string "
                                              "with encoding 'UTF8'"):
-            pydicom.charset.decode(elem, ['ISO_IR 192'])
+            pydicom.charset.decode_element(elem, ['ISO_IR 192'])
             assert u'���������' == elem.value
 
     def test_bad_encoded_single_encoding_enforce_standard(self):
@@ -161,7 +161,7 @@ class TestCharset(object):
         msg = ("'utf.?8' codec can't decode byte 0xc4 in position 0: "
                "invalid continuation byte")
         with pytest.raises(UnicodeDecodeError, match=msg):
-            pydicom.charset.decode(elem, ['ISO_IR 192'])
+            pydicom.charset.decode_element(elem, ['ISO_IR 192'])
 
     def test_code_extensions_not_allowed(self):
         """Test that UTF8 does not allow code extensions"""
@@ -171,8 +171,10 @@ class TestCharset(object):
                "allow code extensions, ignoring: ISO 2022 IR 100, "
                "ISO 2022 IR 144")
         with pytest.warns(UserWarning, match=msg):
-            pydicom.charset.decode(elem, ['ISO_IR 192', 'ISO 2022 IR 100',
-                                          'ISO 2022 IR 144'])
+            pydicom.charset.decode_element(
+                elem,
+                ['ISO_IR 192', 'ISO 2022 IR 100', 'ISO 2022 IR 144']
+            )
             assert u'Buc^Jérôme' == elem.value
 
     def test_convert_encodings_warnings(self):
@@ -196,7 +198,7 @@ class TestCharset(object):
 
         with pytest.warns(UserWarning, match='Failed to decode byte string '
                                              'with encodings: iso-2022-jp'):
-            pydicom.charset.decode(elem, ['ISO 2022 IR 159'])
+            pydicom.charset.decode_element(elem, ['ISO 2022 IR 159'])
             assert u'����������' == elem.value
 
     def test_bad_decoded_multi_byte_encoding_enforce_standard(self):
@@ -208,7 +210,7 @@ class TestCharset(object):
         msg = ("'iso2022_jp' codec can't decode bytes in position 0-3: "
                "illegal multibyte sequence")
         with pytest.raises(UnicodeDecodeError, match=msg):
-            pydicom.charset.decode(elem, ['ISO 2022 IR 159'])
+            pydicom.charset.decode_element(elem, ['ISO 2022 IR 159'])
 
     def test_unknown_escape_sequence(self):
         """Test handling bad encoding for single encoding"""
@@ -217,7 +219,7 @@ class TestCharset(object):
 
         with pytest.warns(UserWarning, match='Found unknown escape sequence '
                                              'in encoded string value'):
-            pydicom.charset.decode(elem, ['ISO_IR 100'])
+            pydicom.charset.decode_element(elem, ['ISO_IR 100'])
             assert u'\x1b-FÄéïíõóéïò' == elem.value
 
     def test_unknown_escape_sequence_enforce_standard(self):
@@ -228,12 +230,12 @@ class TestCharset(object):
                            b'\x1b\x2d\x46\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
         with pytest.raises(ValueError, match='Found unknown escape sequence '
                                              'in encoded string value'):
-            pydicom.charset.decode(elem, ['ISO_IR 100'])
+            pydicom.charset.decode_element(elem, ['ISO_IR 100'])
 
     def test_patched_charset(self):
         """Test some commonly misspelled charset values"""
         elem = DataElement(0x00100010, 'PN', b'Buc^J\xc3\xa9r\xc3\xb4me')
-        pydicom.charset.decode(elem, ['ISO_IR 192'])
+        pydicom.charset.decode_element(elem, ['ISO_IR 192'])
         # correct encoding
         assert u'Buc^Jérôme' == elem.value
 
@@ -242,14 +244,14 @@ class TestCharset(object):
         with pytest.warns(UserWarning,
                           match='Incorrect value for Specific Character Set '
                                 "'ISO IR 192' - assuming 'ISO_IR 192'"):
-            pydicom.charset.decode(elem, ['ISO IR 192'])
+            pydicom.charset.decode_element(elem, ['ISO IR 192'])
             assert u'Buc^Jérôme' == elem.value
 
         elem = DataElement(0x00100010, 'PN', b'Buc^J\xe9r\xf4me')
         with pytest.warns(UserWarning,
                           match='Incorrect value for Specific Character Set '
                                 "'ISO-IR 144' - assuming 'ISO_IR 144'") as w:
-            pydicom.charset.decode(elem, ['ISO_IR 100', 'ISO-IR 144'])
+            pydicom.charset.decode_element(elem, ['ISO_IR 100', 'ISO-IR 144'])
             # make sure no warning is issued for the correct value
             assert 1 == len(w)
 
@@ -258,11 +260,11 @@ class TestCharset(object):
         with pytest.warns(UserWarning,
                           match=u"Unknown encoding 'ISOIR 192' - "
                                 u"using default encoding instead"):
-            pydicom.charset.decode(elem, ['ISOIR 192'])
+            pydicom.charset.decode_element(elem, ['ISOIR 192'])
 
         # Python encoding also can be used directly
         elem = DataElement(0x00100010, 'PN', b'Buc^J\xc3\xa9r\xc3\xb4me')
-        pydicom.charset.decode(elem, ['utf8'])
+        pydicom.charset.decode_element(elem, ['utf8'])
         assert u'Buc^Jérôme' == elem.value
 
     def test_patched_code_extension_charset(self):
@@ -271,7 +273,9 @@ class TestCharset(object):
                            b'Dionysios=\x1b\x2d\x46'
                            b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
         # correct encoding
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126'])
+        pydicom.charset.decode_element(
+            elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126']
+        )
         assert u'Dionysios=Διονυσιος' == elem.value
 
         # patched encoding shall behave correctly, but a warning is issued
@@ -282,7 +286,7 @@ class TestCharset(object):
             elem = DataElement(0x00100010, 'PN',
                                b'Dionysios=\x1b\x2d\x46'
                                b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
-            pydicom.charset.decode(elem,
+            pydicom.charset.decode_element(elem,
                                    ['ISO_2022-IR 100', 'ISO 2022 IR 126'])
             assert u'Dionysios=Διονυσιος' == elem.value
 
@@ -293,7 +297,7 @@ class TestCharset(object):
             elem = DataElement(0x00100010, 'PN',
                                b'Dionysios=\x1b\x2d\x46'
                                b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
-            pydicom.charset.decode(elem,
+            pydicom.charset.decode_element(elem,
                                    ['ISO 2022 IR 100', 'ISO_2022_IR+126'])
             assert u'Dionysios=Διονυσιος' == elem.value
 
@@ -301,11 +305,15 @@ class TestCharset(object):
         """Test that the first value is used if no escape code is given"""
         # regression test for #707
         elem = DataElement(0x00100010, 'PN', b'Buc^J\xe9r\xf4me')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100', 'ISO 2022 IR 144'])
+        pydicom.charset.decode_element(
+            elem, ['ISO 2022 IR 100', 'ISO 2022 IR 144']
+        )
         assert u'Buc^Jérôme' == elem.value
 
         elem = DataElement(0x00081039, 'LO', b'R\xf6ntgenaufnahme')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100', 'ISO 2022 IR 144'])
+        pydicom.charset.decode_element(
+            elem, ['ISO 2022 IR 100', 'ISO 2022 IR 144']
+        )
         assert u'Röntgenaufnahme' == elem.value
 
     def test_single_byte_multi_charset_personname(self):
@@ -313,7 +321,9 @@ class TestCharset(object):
         elem = DataElement(0x00100010, 'PN',
                            b'Dionysios=\x1b\x2d\x46'
                            b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126'])
+        pydicom.charset.decode_element(
+            elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126']
+        )
         assert u'Dionysios=Διονυσιος' == elem.value
 
         # multiple values with different encodings
@@ -322,7 +332,7 @@ class TestCharset(object):
                    b'\x1b\x2d\x4C'
                    b'\xbb\xee\xda\x63\x65\xdc\xd1\x79\x70\xd3')
         elem = DataElement(0x00100060, 'PN', encoded)
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100',
+        pydicom.charset.decode_element(elem, ['ISO 2022 IR 100',
                                       'ISO 2022 IR 144',
                                       'ISO 2022 IR 126'])
         assert [u'Buc^Jérôme', u'Διονυσιος', u'Люкceмбypг'] == elem.value
@@ -332,7 +342,9 @@ class TestCharset(object):
         elem = DataElement(0x00081039, 'LO',
                            b'Dionysios is \x1b\x2d\x46'
                            b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126'])
+        pydicom.charset.decode_element(
+            elem, ['ISO 2022 IR 100', 'ISO 2022 IR 126']
+        )
         assert u'Dionysios is Διονυσιος' == elem.value
 
         # multiple values with different encodings
@@ -341,7 +353,7 @@ class TestCharset(object):
                            b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2\\'
                            b'\x1b\x2d\x4C'
                            b'\xbb\xee\xda\x63\x65\xdc\xd1\x79\x70\xd3')
-        pydicom.charset.decode(elem, ['ISO 2022 IR 100',
+        pydicom.charset.decode_element(elem, ['ISO 2022 IR 100',
                                       'ISO 2022 IR 144',
                                       'ISO 2022 IR 126'])
         assert [u'Buc^Jérôme', u'Διονυσιος', u'Люкceмбypг'] == elem.value
@@ -350,7 +362,7 @@ class TestCharset(object):
     def test_single_byte_code_extensions(self, encoding, decoded, raw_data):
         # single-byte encoding as code extension
         elem = DataElement(0x00081039, 'LO', b'ASCII+' + raw_data)
-        pydicom.charset.decode(elem, ['', encoding])
+        pydicom.charset.decode_element(elem, ['', encoding])
         assert u'ASCII+' + decoded == elem.value
 
     @pytest.mark.parametrize('filename, patient_name', FILE_PATIENT_NAMES)
@@ -402,7 +414,7 @@ class TestCharset(object):
         with pytest.warns(UserWarning,
                           match=u"Unknown encoding 'ISO 2022 IR 146' "
                                 u"- using default encoding instead"):
-            pydicom.charset.decode(
+            pydicom.charset.decode_element(
                 elem, ['ISO 2022 IR 100', 'ISO 2022 IR 146'])
 
     def test_japanese_multi_byte_personname(self):
@@ -437,3 +449,11 @@ class TestCharset(object):
                                 u"s in encoded string"):
             encoded = pydicom.charset.encode_string(u'あaｱア', ['shift_jis'])
             assert b'?a??' == encoded
+
+    def test_deprecated_decode(self):
+        """Test we get a deprecation warning when using charset.decode()."""
+        # Python 3: elem.value is PersonName3, Python 2: elem.value is str
+        elem = DataElement(0x00100010, 'PN', 'CITIZEN')
+        msg = r"'charset.decode\(\)' is deprecated"
+        with pytest.warns(DeprecationWarning, match=msg):
+            pydicom.charset.decode(elem, ['ISO 2022 IR 126'])
diff --git a/pydicom/tests/test_dataelem.py b/pydicom/tests/test_dataelem.py
index 8bd68fe11..5ff1611f0 100644
--- a/pydicom/tests/test_dataelem.py
+++ b/pydicom/tests/test_dataelem.py
@@ -8,6 +8,7 @@ import sys
 
 import pytest
 
+from pydicom import filewriter, config, dcmread
 from pydicom.charset import default_encoding
 from pydicom.dataelem import (
     DataElement,
@@ -15,6 +16,7 @@ from pydicom.dataelem import (
     DataElement_from_raw,
 )
 from pydicom.dataset import Dataset
+from pydicom.filebase import DicomBytesIO
 from pydicom.multival import MultiValue
 from pydicom.tag import Tag
 from pydicom.uid import UID
@@ -32,6 +34,10 @@ class TestDataElement(object):
         self.data_elementCommand = DataElement(0x00000000, 'UL', 100)
         self.data_elementPrivate = DataElement(0x00090000, 'UL', 101)
         self.data_elementRetired = DataElement(0x00080010, 'SH', 102)
+        config.use_none_as_empty_text_VR_value = False
+
+    def teardown(self):
+        config.use_none_as_empty_text_VR_value = False
 
     def test_VM_1(self):
         """DataElement: return correct value multiplicity for VM > 1"""
@@ -397,22 +403,27 @@ class TestDataElement(object):
         ds.decode()
         assert 'UN' == ds[0x30040058].VR
 
-    def test_empty_text_values(self):
+    @pytest.mark.parametrize('use_none, empty_value',
+                             ((True, None), (False, '')))
+    def test_empty_text_values(self, use_none, empty_value):
         """Test that assigning an empty value behaves as expected."""
         def check_empty_text_element(value):
             setattr(ds, tag_name, value)
             elem = ds[tag_name]
             assert bool(elem.value) is False
+            assert 0 == elem.VM
+            assert elem.value == value
+            fp = DicomBytesIO()
+            filewriter.write_dataset(fp, ds)
+            ds_read = dcmread(fp, force=True)
+            assert empty_value == ds_read[tag_name].value
 
         text_vrs = {
-            'AE': 'Receiver',
+            'AE': 'RetrieveAETitle',
             'AS': 'PatientAge',
-            'AT': 'OffendingElement',
             'CS': 'QualityControlSubject',
             'DA': 'PatientBirthDate',
-            'DS': 'PatientWeight',
             'DT': 'AcquisitionDateTime',
-            'IS': 'BeamNumber',
             'LO': 'DataSetSubtype',
             'LT': 'ExtendedCodeMeaning',
             'PN': 'PatientName',
@@ -424,14 +435,14 @@ class TestDataElement(object):
             'UR': 'CodingSchemeURL',
             'UT': 'StrainAdditionalInformation',
         }
+        config.use_none_as_empty_text_VR_value = use_none
         ds = Dataset()
+        ds.is_little_endian = True
         # set value to new element
         for tag_name in text_vrs.values():
             check_empty_text_element(None)
             del ds[tag_name]
-            check_empty_text_element(b'')
-            del ds[tag_name]
-            check_empty_text_element(u'')
+            check_empty_text_element('')
             del ds[tag_name]
             check_empty_text_element([])
             del ds[tag_name]
@@ -439,8 +450,7 @@ class TestDataElement(object):
         # set value to existing element
         for tag_name in text_vrs.values():
             check_empty_text_element(None)
-            check_empty_text_element(b'')
-            check_empty_text_element(u'')
+            check_empty_text_element('')
             check_empty_text_element([])
             check_empty_text_element(None)
 
@@ -451,8 +461,17 @@ class TestDataElement(object):
             setattr(ds, tag_name, value)
             elem = ds[tag_name]
             assert bool(elem.value) is False
+            assert 0 == elem.VM
+            assert elem.value == value
+            fp = DicomBytesIO()
+            filewriter.write_dataset(fp, ds)
+            ds_read = dcmread(fp, force=True)
+            assert ds_read[tag_name].value is None
 
         non_text_vrs = {
+            'AT': 'OffendingElement',
+            'DS': 'PatientWeight',
+            'IS': 'BeamNumber',
             'SL': 'RationalNumeratorValue',
             'SS': 'SelectorSSValue',
             'UL': 'SimpleFrameList',
@@ -467,6 +486,7 @@ class TestDataElement(object):
             'UN': 'SelectorUNValue',
         }
         ds = Dataset()
+        ds.is_little_endian = True
         # set value to new element
         for tag_name in non_text_vrs.values():
             check_empty_binary_element(None)
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
index 142e4fb34..cc35fbba2 100644
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -93,16 +93,16 @@ save_dir = os.getcwd()
 
 class TestReader(object):
     def test_empty_numbers_tag(self):
-        """Tests that an empty tag with a number VR (FL, UL, SL, US,
-        SS, FL, FD, OF) reads as an empty string"""
+        """Test that an empty tag with a number VR (FL, UL, SL, US,
+        SS, FL, FD, OF) reads as ``None``."""
         empty_number_tags_ds = dcmread(empty_number_tags_name)
-        assert '' == empty_number_tags_ds.ExaminedBodyThickness
-        assert '' == empty_number_tags_ds.SimpleFrameList
-        assert '' == empty_number_tags_ds.ReferencePixelX0
-        assert '' == empty_number_tags_ds.PhysicalUnitsXDirection
-        assert '' == empty_number_tags_ds.TagAngleSecondAxis
-        assert '' == empty_number_tags_ds.TagSpacingSecondDimension
-        assert '' == empty_number_tags_ds.VectorGridData
+        assert empty_number_tags_ds.ExaminedBodyThickness is None
+        assert empty_number_tags_ds.SimpleFrameList is None
+        assert empty_number_tags_ds.ReferencePixelX0 is None
+        assert empty_number_tags_ds.PhysicalUnitsXDirection is None
+        assert empty_number_tags_ds.TagAngleSecondAxis is None
+        assert empty_number_tags_ds.TagSpacingSecondDimension is None
+        assert empty_number_tags_ds.VectorGridData is None
 
     def test_UTF8_filename(self):
         utf8_filename = os.path.join(tempfile.gettempdir(), "ДИКОМ.dcm")
@@ -425,12 +425,19 @@ class TestReader(object):
         long_specific_char_set_value = ['ISO 2022IR 100'] * 9
         ds.add(DataElement(0x00080005, 'CS', long_specific_char_set_value))
 
+        msg = (
+            r"Unknown encoding 'ISO 2022IR 100' - using default encoding "
+            r"instead"
+        )
+
         fp = BytesIO()
         file_ds = FileDataset(fp, ds)
-        file_ds.save_as(fp, write_like_original=True)
+        with pytest.warns(UserWarning, match=msg):
+            file_ds.save_as(fp, write_like_original=True)
 
-        ds = dcmread(fp, defer_size=65, force=True)
-        assert long_specific_char_set_value == ds[0x00080005].value
+        with pytest.warns(UserWarning, match=msg):
+            ds = dcmread(fp, defer_size=65, force=True)
+            assert long_specific_char_set_value == ds[0x00080005].value
 
     def test_no_preamble_file_meta_dataset(self):
         """Test correct read of group 2 elements with no preamble."""
diff --git a/pydicom/tests/test_numpy_pixel_data.py b/pydicom/tests/test_numpy_pixel_data.py
index 51a13a67f..dc5c75229 100644
--- a/pydicom/tests/test_numpy_pixel_data.py
+++ b/pydicom/tests/test_numpy_pixel_data.py
@@ -31,6 +31,7 @@ from sys import byteorder
 import pytest
 
 import pydicom
+from pydicom import config
 from pydicom.data import get_testdata_files
 from pydicom.dataset import Dataset
 from pydicom.filereader import dcmread
diff --git a/pydicom/tests/test_rle_pixel_data.py b/pydicom/tests/test_rle_pixel_data.py
index 3444999a0..ba8c6ec80 100644
--- a/pydicom/tests/test_rle_pixel_data.py
+++ b/pydicom/tests/test_rle_pixel_data.py
@@ -27,7 +27,7 @@ import sys
 
 import pytest
 
-from pydicom import dcmread, Dataset
+from pydicom import dcmread, Dataset, config
 import pydicom.config
 from pydicom.data import get_testdata_files
 from pydicom.encaps import defragment_data
diff --git a/pydicom/tests/test_valuerep.py b/pydicom/tests/test_valuerep.py
index 5f1efd348..3d7190f79 100644
--- a/pydicom/tests/test_valuerep.py
+++ b/pydicom/tests/test_valuerep.py
@@ -92,7 +92,7 @@ class TestDS(object):
     """Unit tests for DS values"""
 
     def test_empty_value(self):
-        assert '' == DS(None)
+        assert DS(None) is None
         assert '' == DS('')
 
     def test_float_values(self):
@@ -144,7 +144,7 @@ class TestIS(object):
     """Unit tests for IS"""
 
     def test_empty_value(self):
-        assert '' == IS(None)
+        assert IS(None) is None
         assert '' == IS('')
 
     def test_valid_value(self):
@@ -196,6 +196,7 @@ class TestBadValueRead(object):
         self.tag.is_little_endian = True
         self.tag.is_implicit_VR = False
         self.tag.tag = Tag(0x0010, 0x0020)
+        self.tag.length = 2
         self.default_retry_order = pydicom.values.convert_retry_VR_order
 
     def teardown(self):