Repository: PlasmaPy/PlasmaPy

Issue:
Check for and remove any mutable arguments in functions & methods
Callable objects [should not have mutable default values](https://florimond.dev/blog/articles/2018/08/python-mutable-defaults-are-the-source-of-all-evil/) (like lists, sets, and dictionaries) for functions and methods because unexpected behavior like this can happen:
```
In [2]: def f(a=[]): 
   ...:     a.append(0) 
   ...:     print(a) 
   ...:

In [3]: f()
[0]

In [4]: f() 
[0, 0]
```
We should check for mutable arguments in functions and methods, and then remove any that we do find.  The most likely place for this is in what will soon be `plasmapy.particles` (`plasmapy.atomic` until #742).  It may show up a bit also in `plasmapy.formulary`, perhaps `plasmapy.utils`, and maybe elsewhere.

Thanks to @Ishinomori for bringing this up and making a fix to this in #751!


Discussion:
I can take it as well.
To search for similar issues in the future you can run `pylint plasmapy --disable=all --enable=dangerous-default-value` from top-level dir.
Thank you again!  Running the command you gave shows this happening in the following files.

- `plasmapy/atomic/particle_input.py`
- `plasmapy/atomic/parsing.py`
- `plasmapy/utils/datatype_factory_base.py`
- `plasmapy/utils/pytest_helpers/pytest_helpers.py` (ignore this one for now, as it's getting heavily refactored in #728)
- `plasmapy/utils/pytest_helpers/error_messages.py` (ignore this one too, same reason)
- `plasmapy/formulary/tests/test_collisions.py`
- `plasmapy/formulary/tests/test_parameters.py`
```

Resolving patch:
diff --git a/plasmapy/atomic/parsing.py b/plasmapy/atomic/parsing.py
--- a/plasmapy/atomic/parsing.py
+++ b/plasmapy/atomic/parsing.py
@@ -2,7 +2,7 @@
 
 import re
 import warnings
-from typing import (Union, Dict)
+from typing import (Union, Dict, Optional)
 from numbers import Integral
 import numpy as np
 
@@ -432,12 +432,16 @@ def _reconstruct_ion_symbol(element: str, isotope: Integral = None, Z: Integral
     return nomenclature_dict
 
 
-def _particle_call_string(arg: Union[str, Integral], kwargs: Dict = {}) -> str:
+def _particle_call_string(arg: Union[str, Integral], kwargs: Optional[Dict] = None) -> str:
     """
     Return a `str` that recreates the call to create a particular
     `~plasmapy.atomic.Particle` instance from the input.
     """
+    if kwargs is None:
+        kwargs = {}
+
     return call_string(Particle, arg, kwargs)
+
 #    if kwargs != {}:
 #        keyword_string = ", " + str(kwargs).strip(r"}{'").replace("'", "").replace(":", " =")
 #    else:
diff --git a/plasmapy/atomic/particle_input.py b/plasmapy/atomic/particle_input.py
--- a/plasmapy/atomic/particle_input.py
+++ b/plasmapy/atomic/particle_input.py
@@ -71,9 +71,9 @@ def _category_errmsg(particle, require, exclude, any_of, funcname) -> str:
 
 
 def particle_input(wrapped_function: Callable = None,
-                   require: Union[str, Set, List, Tuple] = set(),
-                   any_of: Union[str, Set, List, Tuple] = set(),
-                   exclude: Union[str, Set, List, Tuple] = set(),
+                   require: Union[str, Set, List, Tuple] = None,
+                   any_of: Union[str, Set, List, Tuple] = None,
+                   exclude: Union[str, Set, List, Tuple] = None,
                    none_shall_pass: bool = False) -> Any:
     """
     Convert arguments to methods and functions to
@@ -230,6 +230,13 @@ def selective_function(particle: Particle):
 
     """
 
+    if exclude is None:
+        exclude = set()
+    if any_of is None:
+        any_of = set()
+    if require is None:
+        require = set()
+
     def decorator(wrapped_function: Callable):
         wrapped_signature = inspect.signature(wrapped_function)
 
diff --git a/plasmapy/utils/datatype_factory_base.py b/plasmapy/utils/datatype_factory_base.py
--- a/plasmapy/utils/datatype_factory_base.py
+++ b/plasmapy/utils/datatype_factory_base.py
@@ -73,13 +73,16 @@ class BasicRegistrationFactory:
     """
 
     def __init__(self, default_widget_type=None,
-                 additional_validation_functions=[], registry=None):
+                 additional_validation_functions=None, registry=None):
 
         if registry is None:
             self.registry = dict()
         else:
             self.registry = registry
 
+        if additional_validation_functions is None:
+            additional_validation_functions = []
+
         self.default_widget_type = default_widget_type
 
         self.validation_functions = (['_factory_validation_function'] +
@@ -160,7 +163,7 @@ def register(self, WidgetType, validation_function=None, is_default=False):
                 if hasattr(WidgetType, vfunc_str):
                     vfunc = getattr(WidgetType, vfunc_str)
 
-                    # check if classmethod: stackoverflow #19227724
+                    # check if classmethod: stackoverflow #19227g724
                     _classmethod = inspect.ismethod(vfunc) and vfunc.__self__ is WidgetType
 
                     if _classmethod:


Test patch:
diff --git a/plasmapy/formulary/tests/test_collisions.py b/plasmapy/formulary/tests/test_collisions.py
--- a/plasmapy/formulary/tests/test_collisions.py
+++ b/plasmapy/formulary/tests/test_collisions.py
@@ -617,11 +617,10 @@ def test_fail1(self):
 
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans,
-                             insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
         assert_can_handle_nparray(impact_parameter_perp, insert_some_nans,
-                                  insert_all_nans, kwargs)
+                                  insert_all_nans, {})
 
     assert np.isclose(Coulomb_logarithm(1 * u.eV, 5 * u.m ** -3, ('e', 'e')),
                       Coulomb_logarithm(11604.5220 * u.K,
@@ -864,10 +863,10 @@ def setup_class(self):
     # TODO: array coulomb log
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
         assert_can_handle_nparray(fundamental_electron_collision_freq, insert_some_nans,
-                                  insert_all_nans, kwargs)
+                                  insert_all_nans, {})
 
 
 class Test_fundamental_ion_collision_freq():
@@ -882,10 +881,10 @@ def setup_class(self):
     # TODO: array coulomb log
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
         assert_can_handle_nparray(fundamental_ion_collision_freq, insert_some_nans,
-                                  insert_all_nans, kwargs)
+                                  insert_all_nans, {})
 
 
 class Test_mean_free_path:
@@ -947,9 +946,9 @@ def test_fail1(self):
 
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
-        assert_can_handle_nparray(mean_free_path, insert_some_nans, insert_all_nans, kwargs)
+        assert_can_handle_nparray(mean_free_path, insert_some_nans, insert_all_nans, {})
 
 
 class Test_Spitzer_resistivity:
@@ -1026,9 +1025,9 @@ def test_zmean(self):
     # TODO vector z_mean
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
-        assert_can_handle_nparray(Spitzer_resistivity, insert_some_nans, insert_all_nans, kwargs)
+        assert_can_handle_nparray(Spitzer_resistivity, insert_some_nans, insert_all_nans, {})
 
 
 class Test_mobility:
@@ -1109,9 +1108,9 @@ def test_zmean(self):
     # TODO vector z_mean
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
-        assert_can_handle_nparray(mobility, insert_some_nans, insert_all_nans, kwargs)
+        assert_can_handle_nparray(mobility, insert_some_nans, insert_all_nans, {})
 
 
 class Test_Knudsen_number:
@@ -1176,9 +1175,9 @@ def test_fail1(self):
 
     @pytest.mark.parametrize("insert_some_nans", [[], ["V"]])
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
-    def test_handle_nparrays(self, insert_some_nans, insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
-        assert_can_handle_nparray(Knudsen_number, insert_some_nans, insert_all_nans, kwargs)
+        assert_can_handle_nparray(Knudsen_number, insert_some_nans, insert_all_nans, {})
 
 
 class Test_coupling_parameter:
@@ -1260,11 +1259,10 @@ def test_zmean(self):
     @pytest.mark.parametrize("insert_all_nans", [[], ["V"]])
     # @pytest.mark.parametrize("kwargs", [{"method": "classical"},
     #                                     {"method": "quantum"},])   # TODO quantum issues
-    def test_handle_nparrays(self, insert_some_nans,
-                             insert_all_nans, kwargs={}):
+    def test_handle_nparrays(self, insert_some_nans, insert_all_nans):
         """Test for ability to handle numpy array quantities"""
         assert_can_handle_nparray(coupling_parameter, insert_some_nans,
-                                  insert_all_nans, kwargs)
+                                  insert_all_nans, {})
 
     @pytest.mark.xfail(reason="see issue https://github.com/PlasmaPy/PlasmaPy/issues/726")
     def test_quantum(self):
diff --git a/plasmapy/formulary/tests/test_parameters.py b/plasmapy/formulary/tests/test_parameters.py
--- a/plasmapy/formulary/tests/test_parameters.py
+++ b/plasmapy/formulary/tests/test_parameters.py
@@ -463,8 +463,10 @@ def test_mean1(self):
                           atol=0.0), errStr
         return
 
-    def test_handle_nparrays(self, kwargs={"kappa": 2}):
+    def test_handle_nparrays(self, kwargs=None):
         """Test for ability to handle numpy array quantities"""
+        if kwargs is None:
+            kwargs = {"kappa": 2}
         assert_can_handle_nparray(kappa_thermal_speed, kwargs=kwargs)