Repository: PlasmaPy/PlasmaPy
Pull request #1013 (2021-02-15 00:09:10)

Remove lambda functions
This pull request removes some `lambda` expressions from `plasmapy.particles` and `plasmapy.utils` because `lambda` expressions cannot be pickled. This partially address #1011.  However, more would need to be done in order to pickle `Particle` instances.  I left the `lambda` expressions in Langmuir stuff to avoid conflicts with #889.  

I propose a new tongue twister:
> I pickled a Particle of pickled PEP8ers.



codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013?src=pr&el=h1) Report
> Merging [#1013](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013?src=pr&el=desc) (7df7b87) into [master](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/6f163e07fb7fa3276a7b09e0f8fb1b0cf3bb3791?el=desc) (6f163e0) will **decrease** coverage by `0.01%`.
> The diff coverage is `93.33%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1013      +/-   ##
==========================================
- Coverage   96.62%   96.61%   -0.02%     
==========================================
  Files          64       64              
  Lines        6249     6258       +9     
==========================================
+ Hits         6038     6046       +8     
- Misses        211      212       +1     
```


| [Impacted Files](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [plasmapy/particles/ionization\_state\_collection.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013/diff?src=pr&el=tree#diff-cGxhc21hcHkvcGFydGljbGVzL2lvbml6YXRpb25fc3RhdGVfY29sbGVjdGlvbi5weQ==) | `91.09% <88.88%> (-0.13%)` | :arrow_down: |
| [plasmapy/particles/particle\_class.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013/diff?src=pr&el=tree#diff-cGxhc21hcHkvcGFydGljbGVzL3BhcnRpY2xlX2NsYXNzLnB5) | `98.46% <100.00%> (ø)` | |
| [plasmapy/utils/decorators/checks.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013/diff?src=pr&el=tree#diff-cGxhc21hcHkvdXRpbHMvZGVjb3JhdG9ycy9jaGVja3MucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013?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/PlasmaPy/PlasmaPy/pull/1013?src=pr&el=footer). Last update [6f163e0...7df7b87](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1013?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/changelog/1013.trivial.rst b/changelog/1013.trivial.rst
new file mode 100644
index 00000000..5a410c8c
--- /dev/null
+++ b/changelog/1013.trivial.rst
@@ -0,0 +1,1 @@
+Remove `lambda` expressions from `plasmapy.particles` and `plasmapy.utils`.
diff --git a/changelog/1015.feature.rst b/changelog/1015.feature.rst
new file mode 100644
index 00000000..a22b5ad2
--- /dev/null
+++ b/changelog/1015.feature.rst
@@ -0,0 +1,3 @@
+Both `plasmapy.particles.CustomParticle` and `plasmapy.particles.DimensionlessParticle`
+now allow users to define a custom symbol via the ``symbol`` keyword argument, which
+can then be accessed by the ``symbol`` attribute in each of these classes.
diff --git a/docs/development/code_guide.rst b/docs/development/code_guide.rst
index 0cbd42c4..e4091e25 100644
--- a/docs/development/code_guide.rst
+++ b/docs/development/code_guide.rst
@@ -49,6 +49,9 @@ Coding Style
 * Use ``Optional[type]`` for type hinted keyword arguments with a
   default value of ``None``.
 
+* Avoid using `lambda` to define functions, as this notation may be
+  unfamiliar to newcomers to Python.
+
 * There should be at most one pun per 1284 lines of code.
 
 pre-commit hooks
diff --git a/plasmapy/particles/ionization_state_collection.py b/plasmapy/particles/ionization_state_collection.py
index 4b582864..3d2ff2c0 100644
--- a/plasmapy/particles/ionization_state_collection.py
+++ b/plasmapy/particles/ionization_state_collection.py
@@ -23,6 +23,10 @@
 from plasmapy.utils.decorators import validate_quantities
 
 
+def _atomic_number_and_mass_number(p: Particle):
+    return (p.atomic_number, p.mass_number if p.isotope else 0)
+
+
 class IonizationStateCollection:
     """
     Describe the ionization state distributions of multiple elements
@@ -163,7 +167,7 @@ def __init__(
                 set_abundances = False
 
         try:
-            self._pars = collections.defaultdict(lambda: None)
+            self._pars = dict()
             self.T_e = T_e
             self.n0 = n0
             self.tol = tol
@@ -526,12 +530,14 @@ def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
             # mass number since we will often want to plot and analyze
             # things and this is the most sensible order.
 
-            sorted_keys = sorted(
-                original_keys,
-                key=lambda k: (
+            def _sort_entries_by_atomic_and_mass_numbers(k):
+                return (
                     particles[k].atomic_number,
                     particles[k].mass_number if particles[k].isotope else 0,
-                ),
+                )
+
+            sorted_keys = sorted(
+                original_keys, key=_sort_entries_by_atomic_and_mass_numbers
             )
 
             _elements_and_isotopes = []
@@ -628,9 +634,8 @@ def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
                     "Invalid inputs to IonizationStateCollection."
                 ) from exc
 
-            _particle_instances.sort(
-                key=lambda p: (p.atomic_number, p.mass_number if p.isotope else 0)
-            )
+            _particle_instances.sort(key=_atomic_number_and_mass_number)
+
             _elements_and_isotopes = [
                 particle.symbol for particle in _particle_instances
             ]
diff --git a/plasmapy/particles/particle_class.py b/plasmapy/particles/particle_class.py
index 0db349e9..57c8a8d4 100644
--- a/plasmapy/particles/particle_class.py
+++ b/plasmapy/particles/particle_class.py
@@ -163,11 +163,6 @@ def json_dict(self) -> dict:
         }
         return json_dictionary
 
-    @property
-    def symbol(self):
-        """Return a string representation of the particle."""
-        return self.__repr__()
-
     def __bool__(self):
         """
         Raise an `~plasmapy.particles.exceptions.ParticleError` because particles
@@ -254,6 +249,11 @@ class Particle(AbstractParticle):
         Raised for attempts at converting a
         `~plasmapy.particles.Particle` object to a `bool`.
 
+    See Also
+    --------
+    ~plasmapy.particles.CustomParticle
+    ~plasmapy.particles.DimensionlessParticle
+
     Examples
     --------
     Particles may be defined using a wide variety of aliases:
@@ -405,7 +405,8 @@ def __init__(
         if Z is not None and not isinstance(Z, Integral):
             raise TypeError("Z is not an integer.")
 
-        self._attributes = defaultdict(lambda: None)
+        # For Python 3.10, change `type(None)` to `types.NoneType`
+        self._attributes = defaultdict(type(None))
         attributes = self._attributes
 
         # Use this set to keep track of particle categories such as
@@ -1709,30 +1710,43 @@ class DimensionlessParticle(AbstractParticle):
     Parameters
     ----------
     mass : positive real number, keyword-only, optional
-        The mass of the dimensionless particle.
+        The mass of the dimensionless particle.  Defaults to `numpy.nan`.
 
     charge : real number, keyword-only, optional
-        The electric charge of the dimensionless particle.
+        The electric charge of the dimensionless particle.  Defaults to
+        `numpy.nan`.
+
+    symbol : str, optional
+        The symbol to be assigned to the dimensionless particle.
+
+    See Also
+    --------
+    ~plasmapy.particles.Particle
+    ~plasmapy.particles.CustomParticle
 
     Notes
     -----
-    If the charge or mass is not specified, then the corresponding value
-    will be set to ``numpy.nan``.
+    `DimensionlessParticle` instances are not considered `ParticleLike`
+    because dimensionless particles cannot uniquely identify a physical
+    particle without normalization information.
 
     Examples
     --------
     >>> from plasmapy.particles import DimensionlessParticle
-    >>> dimensionless_particle = DimensionlessParticle(mass=1.0, charge=-1.0)
+    >>> dimensionless_particle = DimensionlessParticle(mass=1.0, charge=-1.0, symbol="ξ")
     >>> dimensionless_particle.mass
     1.0
     >>> dimensionless_particle.charge
     -1.0
+    >>> dimensionless_particle.symbol
+    'ξ'
     """
 
-    def __init__(self, *, mass: Real = None, charge: Real = None):
+    def __init__(self, *, mass: Real = None, charge: Real = None, symbol: str = None):
         try:
             self.mass = mass
             self.charge = charge
+            self.symbol = symbol
         except Exception as exc:
             raise InvalidParticleError(
                 f"Unable to create a custom particle with a mass of "
@@ -1793,18 +1807,21 @@ def json_dict(self) -> dict:
         {'plasmapy_particle': {'type': 'DimensionlessParticle',
             'module': 'plasmapy.particles.particle_class',
             'date_created': '...',
-            '__init__': {'args': (), 'kwargs': {'mass': 1.0, 'charge': -1.0}}}}
+            '__init__': {'args': (), 'kwargs': {'mass': 1.0, 'charge': -1.0,
+            'symbol': 'DimensionlessParticle(mass=1.0, charge=-1.0)'}}}}
         >>> dimensionless_particle = DimensionlessParticle(mass=1.0)
         >>> dimensionless_particle.json_dict
         {'plasmapy_particle': {'type': 'DimensionlessParticle',
             'module': 'plasmapy.particles.particle_class',
             'date_created': '...',
-            '__init__': {'args': (), 'kwargs': {'mass': 1.0, 'charge': nan}}}}
+            '__init__': {'args': (), 'kwargs': {'mass': 1.0, 'charge': nan,
+            'symbol': 'DimensionlessParticle(mass=1.0, charge=nan)'}}}}
         """
         particle_dictionary = super().json_dict
         particle_dictionary["plasmapy_particle"]["__init__"]["kwargs"] = {
             "mass": self.mass,
             "charge": self.charge,
+            "symbol": self.symbol,
         }
         return particle_dictionary
 
@@ -1846,6 +1863,23 @@ def charge(self, q: Optional[Union[Real, u.Quantity]]):
                 "DimensionlessParticle charge set to NaN", MissingParticleDataWarning
             )
 
+    @property
+    def symbol(self) -> str:
+        """
+        Return the symbol assigned to the dimensionless particle.  If no
+        symbol was defined, then return the value given by `repr`.
+        """
+        return self._symbol
+
+    @symbol.setter
+    def symbol(self, new_symbol: str):
+        if new_symbol is None:
+            self._symbol = repr(self)
+        elif isinstance(new_symbol, str):
+            self._symbol = new_symbol
+        else:
+            raise TypeError("symbol needs to be a string.")
+
 
 class CustomParticle(AbstractParticle):
     """
@@ -1859,12 +1893,15 @@ class CustomParticle(AbstractParticle):
     mass : ~astropy.units.Quantity, optional
         The mass of the custom particle in units of mass.
 
-    charge : ~astropy.units.Quantity or ~numbers.Real
+    charge : ~astropy.units.Quantity or ~numbers.Real, optional
         The electric charge of the custom particle.  If provided as a
         `~astropy.units.Quantity`, then it must be in units of electric
         charge.  If provided as a real number, then it is treated as the
         ratio of the charge to the elementary charge.
 
+    symbol : str, optional
+        The symbol to be assigned to the custom particle.
+
     Raises
     ------
     InvalidParticleError
@@ -1885,17 +1922,20 @@ class CustomParticle(AbstractParticle):
     --------
     >>> from astropy import units as u
     >>> from plasmapy.particles import CustomParticle
-    >>> custom_particle = CustomParticle(mass=1.5e-26 * u.kg, charge=-1)
+    >>> custom_particle = CustomParticle(mass=1.5e-26 * u.kg, charge=-1, symbol="Ξ")
     >>> custom_particle.mass
     <Quantity 1.5e-26 kg>
     >>> custom_particle.charge
     <Quantity -1.60217...e-19 C>
+    >>> custom_particle.symbol
+    'Ξ'
     """
 
-    def __init__(self, mass: u.kg = None, charge: (u.C, Real) = None):
+    def __init__(self, mass: u.kg = None, charge: (u.C, Real) = None, symbol=None):
         try:
             self.mass = mass
             self.charge = charge
+            self.symbol = symbol
         except Exception as exc:
             raise InvalidParticleError(
                 f"Unable to create a custom particle with a mass of "
@@ -1922,23 +1962,26 @@ def json_dict(self) -> dict:
 
         Examples
         --------
-        >>> custom_particle = CustomParticle(mass=5.12 * u.kg, charge=6.2 * u.C)
+        >>> custom_particle = CustomParticle(mass=5.12 * u.kg, charge=6.2 * u.C, symbol="ξ")
         >>> custom_particle.json_dict
         {'plasmapy_particle': {'type': 'CustomParticle',
             'module': 'plasmapy.particles.particle_class',
             'date_created': '...',
-            '__init__': {'args': (), 'kwargs': {'mass': '5.12 kg', 'charge': '6.2 C'}}}}
+            '__init__': {'args': (), 'kwargs': {'mass': '5.12 kg', 'charge': '6.2 C',
+            'symbol': 'ξ'}}}}
         >>> custom_particle = CustomParticle(mass=1.5e-26 * u.kg)
         >>> custom_particle.json_dict
         {'plasmapy_particle': {'type': 'CustomParticle',
             'module': 'plasmapy.particles.particle_class',
             'date_created': '...',
-            '__init__': {'args': (), 'kwargs': {'mass': '1.5e-26 kg', 'charge': 'nan C'}}}}
+            '__init__': {'args': (), 'kwargs': {'mass': '1.5e-26 kg', 'charge': 'nan C',
+            'symbol': 'CustomParticle(mass=1.5e-26 kg, charge=nan C)'}}}}
         """
         particle_dictionary = super().json_dict
         particle_dictionary["plasmapy_particle"]["__init__"]["kwargs"] = {
             "mass": str(self.mass),
             "charge": str(self.charge),
+            "symbol": self.symbol,
         }
         return particle_dictionary
 
@@ -2022,6 +2065,37 @@ def mass(self, m: u.kg):
                     "The mass of a custom particle must have units of mass."
                 ) from exc
 
+    @property
+    def mass_energy(self) -> u.J:
+        """
+        Return the mass energy of the custom particle.
+
+        Examples
+        --------
+        >>> import astropy.units as u
+        >>> custom_particle = CustomParticle(mass = 2e-25 * u.kg, charge = 0 * u.C)
+        >>> custom_particle.mass_energy.to('GeV')
+        <Quantity 112.19177208 GeV>
+        """
+        return (self.mass * const.c ** 2).to(u.J)
+
+    @property
+    def symbol(self) -> str:
+        """
+        Return the symbol assigned to the custom particle.  If no symbol
+        was defined, then return the value given by `repr`.
+        """
+        return self._symbol
+
+    @symbol.setter
+    def symbol(self, new_symbol: str):
+        if new_symbol is None:
+            self._symbol = repr(self)
+        elif isinstance(new_symbol, str):
+            self._symbol = new_symbol
+        else:
+            raise TypeError("symbol needs to be a string.")
+
 
 # TODO: Describe valid particle representations in docstring of particle_like
 
diff --git a/plasmapy/utils/decorators/checks.py b/plasmapy/utils/decorators/checks.py
index 3513a608..8e1287bc 100644
--- a/plasmapy/utils/decorators/checks.py
+++ b/plasmapy/utils/decorators/checks.py
@@ -19,6 +19,7 @@
 from astropy import units as u
 from astropy.constants import c
 from functools import reduce
+from operator import add
 from typing import Any, Dict, List, Tuple, Union
 
 from plasmapy.utils.decorators.helpers import preserve_signature
@@ -724,7 +725,7 @@ def _get_unit_checks(
                 #     (from_unit, to_unit, forward_func, backward_func)
                 #
                 if all(isinstance(el, Equivalency) for el in _equivs):
-                    _equivs = reduce(lambda x, y: x + y, _equivs)
+                    _equivs = reduce(add, _equivs)
                 else:
                     _equivs = self._normalize_equivalencies(_equivs)
 
@@ -991,10 +992,13 @@ def _normalize_equivalencies(equivalencies):
 
         normalized = []
 
+        def return_argument(x):
+            return x
+
         for i, equiv in enumerate(equivalencies):
             if len(equiv) == 2:
                 from_unit, to_unit = equiv
-                a = b = lambda x: x
+                a = b = return_argument
             elif len(equiv) == 3:
                 from_unit, to_unit, a = equiv
                 b = a


Test patch:
diff --git a/plasmapy/particles/tests/test_ionization_collection.py b/plasmapy/particles/tests/test_ionization_collection.py
index d517c1f4..d252307e 100644
--- a/plasmapy/particles/tests/test_ionization_collection.py
+++ b/plasmapy/particles/tests/test_ionization_collection.py
@@ -217,13 +217,10 @@ def test_that_ionic_fractions_are_set_correctly(self, test_name):
         if isinstance(inputs, dict):
             input_keys = list(tests[test_name]["inputs"].keys())
 
-            input_keys = sorted(
-                input_keys,
-                key=lambda k: (
-                    atomic_number(k),
-                    mass_number(k) if Particle(k).isotope else 0,
-                ),
-            )
+            def sort_key(k):
+                return atomic_number(k), mass_number(k) if Particle(k).isotope else 0
+
+            input_keys = sorted(input_keys, key=sort_key)
 
             for element, input_key in zip(elements_actual, input_keys):
                 expected = tests[test_name]["inputs"][input_key]
diff --git a/plasmapy/particles/tests/test_ionization_state.py b/plasmapy/particles/tests/test_ionization_state.py
index 9eaf2b21..5d1b4106 100644
--- a/plasmapy/particles/tests/test_ionization_state.py
+++ b/plasmapy/particles/tests/test_ionization_state.py
@@ -1,6 +1,8 @@
 import astropy.units as u
 import collections
 import numpy as np
+import os
+import pickle
 import pytest
 
 from plasmapy.particles import (
diff --git a/plasmapy/particles/tests/test_particle_class.py b/plasmapy/particles/tests/test_particle_class.py
index b32f1dcb..3a0fde47 100644
--- a/plasmapy/particles/tests/test_particle_class.py
+++ b/plasmapy/particles/tests/test_particle_class.py
@@ -3,6 +3,8 @@
 import io
 import json
 import numpy as np
+import os
+import pickle
 import pytest
 
 from astropy import constants as const
@@ -928,6 +930,20 @@ def test_customized_particles(cls, kwargs, attr, expected):
         )
 
 
+@pytest.mark.parametrize(
+    "cls, symbol, expected",
+    [
+        (CustomParticle, None, "CustomParticle(mass=nan kg, charge=nan C)"),
+        (CustomParticle, "η", "η"),
+        (DimensionlessParticle, None, "DimensionlessParticle(mass=nan, charge=nan)"),
+        (DimensionlessParticle, "η", "η"),
+    ],
+)
+def test_custom_particle_symbol(cls, symbol, expected):
+    instance = cls(symbol=symbol)
+    assert instance.symbol == expected
+
+
 customized_particle_errors = [
     (DimensionlessParticle, {"mass": -1e-36}, InvalidParticleError),
     (DimensionlessParticle, {"mass": [1, 1]}, InvalidParticleError),
@@ -1007,35 +1023,45 @@ def test_customized_particle_repr(cls, kwargs, expected_repr):
         )
 
 
+@pytest.mark.parametrize("cls", [CustomParticle, DimensionlessParticle])
+@pytest.mark.parametrize("not_a_str", [1, u.kg])
+def test_typeerror_redefining_symbol(cls, not_a_str):
+    """Test that the symbol attribute cannot be set to something besides a string"""
+    instance = cls()
+    with pytest.raises(TypeError):
+        instance.symbol = not_a_str
+
+
 custom_particles_from_json_tests = [
     (
         DimensionlessParticle,
-        {"mass": 5.2, "charge": 6.3},
+        {"mass": 5.2, "charge": 6.3, "symbol": "ξ"},
         '{"plasmapy_particle": {"type": "DimensionlessParticle", \
         "module": "plasmapy.particles.particle_class", \
         "date_created": "...", "__init__": { \
             "args": [], \
-            "kwargs": {"mass": 5.2, "charge": 6.3}}}}',
+            "kwargs": {"mass": 5.2, "charge": 6.3, "symbol": "ξ"}}}}',
         None,
     ),
     (
         DimensionlessParticle,
-        {"mass": 5.2},
+        {"mass": 5.2, "symbol": "ξ"},
         '{"plasmapy_particle": {"type": "DimensionlessParticle", \
         "module": "plasmapy.particles.particle_class", \
         "date_created": "...", "__init__": { \
             "args": [], \
-            "kwargs": {"mass": 5.2, "charge": NaN}}}}',
+            "kwargs": {"mass": 5.2, "charge": NaN, "symbol": "ξ"}}}}',
         None,
     ),
     (
         CustomParticle,
-        {"mass": 5.12 * u.kg, "charge": 6.2 * u.C},
+        {"mass": 5.12 * u.kg, "charge": 6.2 * u.C, "symbol": "ξ"},
         '{"plasmapy_particle": {"type": "CustomParticle", \
         "module": "plasmapy.particles.particle_class", \
         "date_created": "...", "__init__": { \
             "args": [], \
-            "kwargs": {"mass": "5.12 kg", "charge": "6.2 C"}}}}',
+            "kwargs": {"mass": "5.12 kg", "charge": "6.2 C", '
+        '"symbol": "ξ"}}}}',
         None,
     ),
     (
@@ -1256,21 +1282,21 @@ def test_particles_from_json_file(cls, kwargs, json_string, expected_exception):
     ),
     (
         CustomParticle,
-        {"mass": 5.12 * u.kg, "charge": 6.2 * u.C},
+        {"mass": 5.12 * u.kg, "charge": 6.2 * u.C, "symbol": "ξ"},
         '{"plasmapy_particle": {"type": "CustomParticle", \
         "module": "plasmapy.particles.particle_class", \
         "date_created": "...", "__init__": {\
             "args": [], \
-            "kwargs": {"mass": "5.12 kg", "charge": "6.2 C"}}}}',
+            "kwargs": {"mass": "5.12 kg", "charge": "6.2 C", "symbol": "ξ"}}}}',
     ),
     (
         DimensionlessParticle,
-        {"mass": 5.2, "charge": 6.3},
+        {"mass": 5.2, "charge": 6.3, "symbol": "ξ"},
         '{"plasmapy_particle": {"type": "DimensionlessParticle",\
         "module": "plasmapy.particles.particle_class",\
         "date_created": "...", "__init__": {\
             "args": [], \
-            "kwargs": {"mass": 5.2, "charge": 6.3}}}}',
+            "kwargs": {"mass": 5.2, "charge": 6.3, "symbol": "ξ"}}}}',
     ),
 ]
 
diff --git a/plasmapy/particles/tests/test_pickling.py b/plasmapy/particles/tests/test_pickling.py
new file mode 100644
index 00000000..3a685570
--- /dev/null
+++ b/plasmapy/particles/tests/test_pickling.py
@@ -0,0 +1,47 @@
+"""Test that objects in `plasmapy.particles` can be pickled."""
+
+import astropy.units as u
+import os
+import pickle
+import pytest
+
+from plasmapy.particles.ionization_state import IonicFraction, IonizationState
+from plasmapy.particles.ionization_state_collection import IonizationStateCollection
+from plasmapy.particles.particle_class import (
+    CustomParticle,
+    DimensionlessParticle,
+    Particle,
+)
+
+
+class TestPickling:
+    """
+    Test that different objects in `plasmapy.particles` can be pickled.
+    """
+
+    xfail = pytest.mark.xfail(reason="see issue #1011")
+
+    @pytest.mark.parametrize(
+        "instance",
+        [
+            CustomParticle(mass=1 * u.kg, charge=1 * u.C),
+            DimensionlessParticle(mass=5, charge=5),
+            pytest.param(Particle("p+"), marks=xfail),
+            pytest.param(IonicFraction("p+", 0.1, 1e9 * u.m ** -3), marks=xfail),
+            pytest.param(IonizationState("H", [0.5, 0.5]), marks=xfail),
+            pytest.param(IonizationStateCollection({"H": [0.5, 0.5]}), marks=xfail),
+        ],
+    )
+    def test_pickling(self, instance, tmp_path):
+        """
+        Test that different objects contained within `plasmapy.particles`
+        can be pickled and unpickled.
+        """
+        filename = tmp_path / "pickled_particles.p"
+        with open(filename, "wb") as pickle_file:
+            pickle.dump(instance, pickle_file)
+
+        with open(filename, "rb") as pickle_file:
+            loaded_particle = pickle.load(pickle_file)
+
+        assert str(instance) == str(loaded_particle)