Repository: PlasmaPy/PlasmaPy

Issue:
Decide on behavior of `ParticleList` when it is provided with a string
`ParticleList` accepts iterables.  When provided with a `str`, `ParticleList` will attempt to create a `Particle` or `CustomParticle` from each of the individual characters in the string.  This could be a helpful shortcut in some cases, but lead to unexpected behavior at other times.
```Python
>>> from plasmapy.particles import ParticleList
>>> ParticleList("pe")  # potentially useful shortcut
ParticleList(['p+', 'e-'])
>>> ParticleList("He")  # might expect He instead of H and e⁻
ParticleList(['H', 'e-'])
```
We have a few options for how `ParticleList` should behave, including but not limited to:

 1. Keep the current behavior (to be consistent with how `list` behaves)
 2. Make `ParticleList` raise a `TypeError` when provided with a string

If we keep the current behavior, we would also have the options of:

 3. Include an admonition in the docstring for `ParticleList` about this behavior 
 4. Include an example in the documentation so users would know that this could happen  
 5. Issue a warning for cases like `ParticleList("He")` where the whole string is a valid particle representation.  This could be done with something more or less like:
    ```Python
    if isinstance(particles, str) and len(particles) > 1:
        try:
            Particle(particles)
        except InvalidParticleError:
            pass
        else:
            warnings.warn("...")
     ```


Discussion:
Oh...and for particles that can be represented by a single letter, things serendipitously work out as expected:
```Python
>>> ParticleList("p")
ParticleList(['p+'])
```
I came across this while looking at ParticleLists for #974. 

My vote would be for changing the behavior in favor of a third option: if a string is provided, treat it as a single particle in a ParticleList of only one element. If the string is not a valid Particle, let Particle raise that error.  My second choice would be to have ParticleList raise an exception if it's given a string. 

IMO, the biggest risk is the example `ParticleList("He")` you mentioned above. I think interpreting this as a hydrogen-electron plasma is really unintuitive and subtle for users who aren't familiar with the way Python deals with strings.

Resolving patch:
diff --git a/plasmapy/particles/particle_collections.py b/plasmapy/particles/particle_collections.py
--- a/plasmapy/particles/particle_collections.py
+++ b/plasmapy/particles/particle_collections.py
@@ -106,11 +106,18 @@ class ParticleList(collections.UserList):
     @staticmethod
     def _list_of_particles_and_custom_particles(
         particles: Optional[Iterable[ParticleLike]],
-    ) -> List[Union[Particle, CustomParticle]]:  # TODO #687
+    ) -> List[Union[Particle, CustomParticle]]:
         """
         Convert an iterable that provides |particle-like| objects into a
         `list` containing |Particle| and |CustomParticle| instances.
         """
+        if isinstance(particles, str):
+            raise TypeError(
+                "ParticleList does not accept strings, but does accept "
+                "lists and tuples containing strings. Did you mean to "
+                f"do `ParticleList([{particles!r}])` instead?"
+            )
+
         new_particles = []
         if particles is None:
             return new_particles


Test patch:
diff --git a/plasmapy/particles/tests/test_exceptions.py b/plasmapy/particles/tests/test_exceptions.py
--- a/plasmapy/particles/tests/test_exceptions.py
+++ b/plasmapy/particles/tests/test_exceptions.py
@@ -943,7 +943,6 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
     [particle_mass, ["fe-56 1+"], {}, pytest.raises(InvalidParticleError)],
     [is_stable, ["hydrogen-444444"], {}, pytest.raises(InvalidParticleError)],
     [is_stable, ["hydrogen", 0], {}, pytest.raises(InvalidParticleError)],
-    [is_stable, [""], {}, pytest.raises(ParticleError)],
     [is_stable, ["pb-209"], {}, pytest.raises(InvalidParticleError)],
     [is_stable, ["h"], {}, pytest.raises(InvalidParticleError)],
     [is_stable, ["He"], {}, pytest.raises(InvalidIsotopeError)],
diff --git a/plasmapy/particles/tests/test_particle_collections.py b/plasmapy/particles/tests/test_particle_collections.py
--- a/plasmapy/particles/tests/test_particle_collections.py
+++ b/plasmapy/particles/tests/test_particle_collections.py
@@ -492,3 +492,9 @@ def test_particle_list_with_no_arguments():
     empty_particle_list = ParticleList()
     assert isinstance(empty_particle_list, ParticleList)
     assert len(empty_particle_list) == 0
+
+
+@pytest.mark.parametrize("arg", ["", "H", "He"])
+def test_particle_list_string_exception(arg):
+    with pytest.raises(TypeError):
+        ParticleList(arg)