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:
github-actions[bot]: [changelog guide]: https://docs.plasmapy.org/en/latest/contributing/changelog_guide.html
[coding guide]: https://docs.plasmapy.org/en/latest/contributing/coding_guide.html
[doc guide]: https://docs.plasmapy.org/en/latest/contributing/doc_guide.html
[testing guide]: https://docs.plasmapy.org/en/latest/contributing/testing_guide.html

Thank you for contributing to PlasmaPy! The project's future depends deeply on contributors like you, so we deeply appreciate it! 🌱 The following checklist will be used by the code reviewer to help guide the code review process.

- Overall
  - [ ] Does the PR do what it intends to do?
  - [ ] Except for very minor changes, is a changelog entry included and consistent with the [changelog guide]?
  - [ ] Are the continuous integration checks passing? (Most linter problems can be automagically fixed by commenting on this PR with `pre-commit.ci autofix`.)
- Code
  - [ ] Is new/updated code understandable and consistent with the [coding guide]?
  - [ ] Are there ways to greatly simplify the implementation?
  - [ ] Are there any large functions that should be split up into shorter functions?
- Tests
  - [ ] Are tests added/updated as required, and consistent with the [testing guide]?
  - [ ] Are the tests understandable?
  - [ ] Do the tests cover all important cases?
- Docs
  - [ ] Are docs added/updated as required, and consistent with the [doc guide]?
  - [ ] Are the docs understandable?
  - [ ] Do the docs show up correctly in the preview, including Jupyter notebooks?

codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1892?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) Report
Base: **98.31**% // Head: **98.31**% // Increases project coverage by **`+0.00%`** :tada:
> Coverage data is based on head [(`322c96d`)](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1892?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) compared to base [(`dfae3e2`)](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/dfae3e2081c31d1cf3fada7bd991db1acf17593a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).
> Patch coverage: 100.00% of modified lines in pull request are covered.

> :exclamation: Current head 322c96d differs from pull request most recent head c622072. Consider uploading reports for the commit c622072 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1892   +/-   ##
=======================================
  Coverage   98.31%   98.31%           
=======================================
  Files          95       95           
  Lines        8404     8406    +2     
=======================================
+ Hits         8262     8264    +2     
  Misses        142      142           
```


| [Impacted Files](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1892?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) | Coverage Δ | |
|---|---|---|
| [plasmapy/particles/particle\_collections.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1892?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy#diff-cGxhc21hcHkvcGFydGljbGVzL3BhcnRpY2xlX2NvbGxlY3Rpb25zLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1892?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).

namurphy: Thank you for the quick review!  

We could potentially revisit this in the future if there's enough demand for being able to do things like `ParticleList("ep")`, which would admittedly be convenient, though not as readable. 

Patch:
diff --git a/changelog/1892.breaking.rst b/changelog/1892.breaking.rst
new file mode 100644
index 00000000..164abd66
--- /dev/null
+++ b/changelog/1892.breaking.rst
@@ -0,0 +1,5 @@
+Made |ParticleList| raise a `TypeError` when provided with a string.
+This change was made to avoid potentially ambiguous situations like
+``ParticleList("He")`` which was previously equivalent to
+``ParticleList(["H", "e"])`` instead of the possibly expected value of
+``ParticleList(["He"])``.
diff --git a/plasmapy/particles/particle_collections.py b/plasmapy/particles/particle_collections.py
index 6b942d43..61b79e3c 100644
--- 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
index dd1eb671..1ccf9ccd 100644
--- 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
index c2acaeff..2ef24480 100644
--- 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)