Repository: PlasmaPy/PlasmaPy
Pull request #920 (2020-10-12 01:11:03)

Test helper refactoring 2: Refactor and move functionality for generating call strings used in error messages
The second spin-off PR from #726.  This should be reviewed and merged after #919.

We currently have a function `call_string` which generates a string that can reproduce a particular call.  This function is currently used in test helper functionality when generating error messages.  Its potential uses go beyond creating error messages, though, such as in `__repr__` methods.  This PR moves that functionality to `plasmapy.utils.formatting`, and creates functions for reproducing the call strings of functions and attributes/methods of class instances.



codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/920?src=pr&el=h1) Report
> Merging [#920](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/920?src=pr&el=desc) into [master](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/6d5651cb9cfb7bd010c8e81c45a1bffeb9c4e95d?el=desc) will **decrease** coverage by `0.26%`.
> The diff coverage is `72.58%`.

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

```diff
@@            Coverage Diff             @@
##           master     #920      +/-   ##
==========================================
- Coverage   96.21%   95.94%   -0.27%     
==========================================
  Files          60       62       +2     
  Lines        5417     5479      +62     
==========================================
+ Hits         5212     5257      +45     
- Misses        205      222      +17     
```


| [Impacted Files](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/920?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [plasmapy/utils/formatting/formatting.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/920/diff?src=pr&el=tree#diff-cGxhc21hcHkvdXRpbHMvZm9ybWF0dGluZy9mb3JtYXR0aW5nLnB5) | `72.13% <72.13%> (ø)` | |
| [plasmapy/utils/formatting/\_\_init\_\_.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/920/diff?src=pr&el=tree#diff-cGxhc21hcHkvdXRpbHMvZm9ybWF0dGluZy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |

------

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

namurphy:  - I removed colorama as a dependency, in part because it sometimes led to weird formatting in terminals.  
 - I sorted imports with `isort` and reformatted with `black` for a small number of files elsewhere in the package.
namurphy: My to do list...
 - [ ] Move `formatting.py` to its own file in utils, rather than in a directory in utils
 - [ ] Decide on a name for `formatting.py`
 - [ ] Remove deprecation stuff from this PR as its out-of-scope
 - [ ] Put `__all__` above imports everywhere
namurphy: @rocco8773 — thanks again for the review!  I applied most of the suggestions so it's ready for another round when you have a chance.  It'd be particularly helpful if you could double check that the `docs/api_static` stuff is being handled correctly.  

Patch:
diff --git a/Dockerfile.documentation b/Dockerfile.documentation
index 1e406675..cb4e47c6 100644
--- a/Dockerfile.documentation
+++ b/Dockerfile.documentation
@@ -1,4 +1,4 @@
-FROM circleci/python:3.6
+FROM circleci/python:3.7
 RUN sudo apt update && sudo apt install texlive texlive-xetex texlive-fonts-extra texlive-latex-extra texlive-plain-extra latexmk graphviz
 ADD ./requirements /
 RUN pip install --user -r /automated-documentation-tests.txt
diff --git a/README.md b/README.md
index 9638737e..f3830c5b 100644
--- a/README.md
+++ b/README.md
@@ -2,40 +2,59 @@
 
 # PlasmaPy
 
-[![PyPI version](https://badge.fury.io/py/plasmapy.svg)](https://badge.fury.io/py/plasmapy)
+[![PyPI version](https://img.shields.io/pypi/v/plasmapy?style=flat&logo=pypi)](https://pypi.org/project/plasmapy/)
+[![Conda version](https://img.shields.io/conda/v/conda-forge/plasmapy?style=flat&logo=anaconda)](https://img.shields.io/conda/v/conda-forge/plasmapy)
+[![PyPI version](https://img.shields.io/pypi/pyversions/plasmapy?style=flat&logo=python)](https://img.shields.io/pypi/pyversions/plasmapy?style=plastic)
 [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](./LICENSE.md)
-[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1436011.svg)](https://doi.org/10.5281/zenodo.1436011)
 
-[![Build Status](https://travis-ci.org/PlasmaPy/PlasmaPy.svg?branch=master)](https://travis-ci.org/PlasmaPy/PlasmaPy)
-[![Build Status](https://dev.azure.com/plasmapy/PlasmaPy/_apis/build/status/PlasmaPy.PlasmaPy?branchName=master)](https://dev.azure.com/plasmapy/PlasmaPy/_build/latest?definitionId=2&branchName=master)
-[![Build status](https://ci.appveyor.com/api/projects/status/hbduy62sqrvy8rn7?svg=true)](https://ci.appveyor.com/project/namurphy/plasmapy)
+[![Matrix](https://img.shields.io/badge/Matrix-join%20chat-blueviolet?style=flat&logo=matrix)](https://app.element.io/#/room/#plasmapy:openastronomy.org)
+[![YouTube](https://img.shields.io/badge/Twitter%20-follow-red?style=flat&logo=twitter)](https://www.youtube.com/channel/UCSH6qzslhqIZKTAJmHPxIxw)
+[![YouTube](https://img.shields.io/badge/YouTube%20-subscribe-red?style=flat&logo=youtube)](https://www.youtube.com/channel/UCSH6qzslhqIZKTAJmHPxIxw)
+
+[![GitHub Actions — CI](https://github.com/PlasmaPy/PlasmaPy/workflows/CI/badge.svg)](https://github.com/PlasmaPy/PlasmaPy/actions?query=workflow%3ACI+branch%3Amaster)
+[![GitHub Actions — Style linters](https://github.com/PlasmaPy/PlasmaPy/workflows/Style%20linters/badge.svg)](https://github.com/PlasmaPy/PlasmaPy/actions?query=workflow%3AStyle-linters+branch%3Amaster)
 [![codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/branch/master/graph/badge.svg)](https://codecov.io/gh/PlasmaPy/PlasmaPy)
-[![Documentation Status](https://readthedocs.org/projects/plasmapy/badge/?version=latest)](http://plasmapy.readthedocs.io/en/latest/?badge=latest)
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/PlasmaPy/PlasmaPy/master?filepath=plasmapy%2Fexamples)
-[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
+[![Read the Docs Status](https://readthedocs.org/projects/plasmapy/badge/?version=latest&logo=twitter)](http://plasmapy.readthedocs.io/en/latest/?badge=latest)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/PlasmaPy/PlasmaPy/master?filepath=docs/notebooks)
 
-[![Matrix](https://matrix.to/img/matrix-badge.svg)](https://app.element.io/#/room/#plasmapy:openastronomy.org)
-[![Gitter (bridged to Matrix)](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/PlasmaPy/Lobby)
-[![astropy](http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat)](http://www.astropy.org/)
+[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1436011.svg)](https://doi.org/10.5281/zenodo.1436011)
+[![astropy](http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat&logo=astropy)](http://www.astropy.org/)
+[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
 [![Open Source Helpers](https://www.codetriage.com/plasmapy/plasmapy/badges/users.svg)](https://www.codetriage.com/plasmapy/plasmapy)
 
-PlasmaPy is an open source community developed Python 3.6+ package for
-plasma physics in the early stages of development.  PlasmaPy intends to
-be for plasmas what [Astropy](https://github.com/astropy/astropy) is for
-astronomy — a collection of functionality commonly used and shared
-between plasma physicists and researchers globally, running within and
-leveraging the open source scientific Python ecosystem.  The goals of
-this project are more thoroughly described in our [vision
-statement](http://docs.plasmapy.org/en/stable/about/vision_statement.html)
-and [this recent reference](https://doi.org/10.5281/zenodo.1238132).
-We are in the process of writing [online
+[PlasmaPy](https://www.plasmapy.org/) is an open source, community-developed
+Python 3.7+ package for plasma science. PlasmaPy intends to be for plasma
+science what [Astropy](https://github.com/astropy/astropy) is for astronomy
+— a collection of functionality commonly used and shared between plasma
+scientists and researchers globally, running within and leveraging the
+open source scientific Python ecosystem. The goals of this project are
+more thoroughly described in [this recent video](https://youtu.be/E8RwQF5wcXM).
+Current functionality is described in [PlasmaPy's online
 documentation](http://docs.plasmapy.org/en/latest/).
 
 We created a guide on [contributing to PlasmaPy](http://docs.plasmapy.org/en/stable/CONTRIBUTING.html)
-and have a [Code of Conduct](http://docs.plasmapy.org/en/stable/CODE_OF_CONDUCT.html).
+and have a [code of conduct](http://docs.plasmapy.org/en/stable/CODE_OF_CONDUCT.html).
 New contributors are very welcome!
 
-# Feedback and communication
+# Installation
+
+If you have [installed Python](https://wiki.python.org/moin/BeginnersGuide/Download),
+you can install PlasmaPy from [pip](https://pypi.org/project/pip/)
+via
+```Shell
+python -m pip install plasmapy
+```
+If you have
+[installed conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html),
+then you can also get PlasmaPy from
+```Shell
+conda install -c conda-forge plasmapy
+```
+To contribute
+to the package, check out [our instructions on installing PlasmaPy from
+source](http://docs.plasmapy.org/en/stable/install.html#building-and-installing-from-source-code).
+
+# Community
 
 ## [Matrix chat](https://app.element.io/#/room/#plasmapy:openastronomy.org)
 
@@ -44,6 +63,25 @@ ## [Matrix chat](https://app.element.io/#/room/#plasmapy:openastronomy.org)
 [Matrix](https://app.element.io/#/room/#plasmapy:openastronomy.org)/[Gitter](https://gitter.im/PlasmaPy/Lobby)
 channel. Both of these are the same chat channel; Gitter uses a bridge to link the two.
 
+## [Weekly](https://calendar.google.com/calendar?cid=bzVsb3ZkcW0zaWxsam00ZTlrMDd2cmw5bWdAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) [community meetings](https://meet.jit.si/plasmapy)
+
+We have weekly community meetings in the
+[PlasmaPy room on Jitsi](https://meet.jit.si/plasmapy).
+The schedule of our community meetings is on our [calendar](https://calendar.google.com/calendar?cid=bzVsb3ZkcW0zaWxsam00ZTlrMDd2cmw5bWdAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ), and you may access the [minutes and
+agendas](https://drive.google.com/drive/folders/0ByPG8nie6fTPV1FQUEkzMTgtRTg?usp=sharing).
+Any last minute changes will be discussed on
+[Matrix](https://app.element.io/app/#/room/#plasmapy:openastronomy.org).
+As of January 2021, our meetings are on Tuesdays at
+[19:00 UTC](http://time.unitarium.com/utc/6pm).
+Come discuss plasma software with us!
+
+## [Weekly office hours](http://www.plasmapy.org/meetings/office_hours/)
+
+PlasmaPy's weekly [office hours](http://www.plasmapy.org/meetings/office_hours/)
+on Thursdays at [19:00 UTC](http://time.unitarium.com/utc/6pm)
+are an opportunity to chat with active members of the PlasmaPy
+community about the package and project.
+
 ## [GitHub discussions](https://github.com/PlasmaPy/PlasmaPy/discussions)
 
 We're now trying out GitHub discussions for more varied topics that aren't
@@ -52,8 +90,9 @@ ## [GitHub discussions](https://github.com/PlasmaPy/PlasmaPy/discussions)
 
 ## [Mailing list](https://groups.google.com/forum/#!forum/plasmapy)
 
-We also have a [mailing list](https://groups.google.com/forum/#!forum/plasmapy)
-that serves as a less volatile discussion forum.
+You can subscribe to our low-volume
+[mailing list](https://groups.google.com/forum/#!forum/plasmapy)
+to receive PlasmaPy newsletters and other announcements.
 
 ## [Suggestion box](https://docs.google.com/forms/d/e/1FAIpQLSdT3O5iHZrLJRuavFyzoR23PGy0Prfzx2SQOcwJGWtvHyT2lw/viewform?usp=sf_link)
 
@@ -63,50 +102,32 @@ ## [Suggestion box](https://docs.google.com/forms/d/e/1FAIpQLSdT3O5iHZrLJRuavFyz
 a feature/topic for consideration. These will be reposted on the mailing list
 or directly in GitHub issues, as appropriate, for further discussion.
 
-## [Weekly](https://calendar.google.com/calendar?cid=bzVsb3ZkcW0zaWxsam00ZTlrMDd2cmw5bWdAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) [community meetings](https://meet.jit.si/plasmapy)
-
-We have weekly community meetings in the
-[PlasmaPy room on Jitsi](https://meet.jit.si/plasmapy).
-The schedule of our community meetings is on our [calendar](https://calendar.google.com/calendar?cid=bzVsb3ZkcW0zaWxsam00ZTlrMDd2cmw5bWdAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ), and you may access the [minutes and
-agendas](https://drive.google.com/drive/folders/0ByPG8nie6fTPV1FQUEkzMTgtRTg?usp=sharing).
-Any last minute changes will be discussed on
-[Matrix](https://app.element.io/app/#/room/#plasmapy:openastronomy.org).
-As of April 2020, our meetings are on Tuesdays at
-[19:00 UTC](http://time.unitarium.com/utc/6pm).
-Come discuss plasma software with us!
-
-# Installation
-
-You can get PlasmaPy from pip via `pip install plasmapy`. To contribute
-to the package, check out [our instructions on installing PlasmaPy from
-source](http://docs.plasmapy.org/en/stable/install.html#building-and-installing-from-source-code).
-
-You can also get PlasmaPy from `conda` via `conda install -c conda-forge plasmapy`.
-
-Like most scientific Python packages, PlasmaPy probably runs better on the
-[Anaconda distribution](https://www.anaconda.com/downloads).
-
-PlasmaPy requires Python 3.6+ and is [not compatible with
-Python 2](https://pythonclock.org/).
-
 # License
 
-PlasmaPy is licensed under a 3-clause BSD license with added protections
-against software patents — see the [``LICENSE.md``](LICENSE.md) file in
-the top-level directory.
+PlasmaPy is permissively licensed under a
+[3-clause BSD license with added protections
+against software patents](LICENSE.md).
 
 # Citing PlasmaPy
 
-If you use PlasmaPy in a research publication, we ask that you cite the
-Zenodo record for the specific version of PlasmaPy that you used.
-Please see the [``docs/about/citation.rst``](./docs/about/citation.rst)
-file for more detailed instructions on how to cite PlasmaPy.
+An [emerging best practice for software
+citation](https://doi.org/10.7717/peerj-cs.86) is to cite the _specific
+version_ of each software package used in a research project (instead of
+only citing a journal article, website, or GitHub repository). The
+citation should include a persistent identifier that uniquely identifies
+which version of the software was used. We therefore ask that you cite
+the specific version of PlasmaPy used in your research project. Releases
+of PlasmaPy are available in the
+[PlasmaPy community](https://zenodo.org/communities/plasmapy) on
+[Zenodo](https://zenodo.org/), along with many other PlasmaPy resources.
+Please check our documentation for more detailed [citation
+instructions](./docs/about/citation.rst).
 
 # Acknowledgements
 
 Early development on PlasmaPy was supported in part by the U.S.
 Department of Energy, the Smithsonian Institution, and Google Summer of
-Code.  Ongoing PlasmaPy development is being supported through a
+Code. Ongoing PlasmaPy development is being supported through a
 collaborative award from the U.S. National Science Foundation's
 Cyberinfrastructure for Sustained Scientific Innovation program and a
-NASA HDEE grant.
+NASA Heliophysics Data Environment Enhancements award.
diff --git a/changelog/920.breaking.rst b/changelog/920.breaking.rst
new file mode 100644
index 00000000..8fcba4ca
--- /dev/null
+++ b/changelog/920.breaking.rst
@@ -0,0 +1,2 @@
+Moved and refactored error message formatting functionality from
+``plasmapy.utils.error_messages`` to `plasmapy.utils.code_repr`.
diff --git a/changelog/920.feature.rst b/changelog/920.feature.rst
new file mode 100644
index 00000000..0beff685
--- /dev/null
+++ b/changelog/920.feature.rst
@@ -0,0 +1,3 @@
+Added functions in `plasmapy.utils.code_repr` to reproduce strings
+that represent a call to a method or attribute of an object. These
+functions are used, for example, in error messages.
diff --git a/changelog/920.trivial.rst b/changelog/920.trivial.rst
new file mode 100644
index 00000000..2cd22008
--- /dev/null
+++ b/changelog/920.trivial.rst
@@ -0,0 +1,1 @@
+Removed `colorama` as a dependency.
diff --git a/changelog/984.feature.rst b/changelog/984.feature.rst
new file mode 100644
index 00000000..04d22a72
--- /dev/null
+++ b/changelog/984.feature.rst
@@ -0,0 +1,6 @@
+The `~plasmapy.particles.particle_class.Particle` class now contains an
+attribute named ``symbol`` that is intended to replace ``particle``. The
+``symbol`` attribute has been added as a property to
+`~plasmapy.particles.particle_class.AbstractParticle`,
+`~plasmapy.particles.particle_class.CustomParticle`, and
+`~plasmapy.particles.particle_class.DimensionlessParticle`.
diff --git a/changelog/984.removal.rst b/changelog/984.removal.rst
new file mode 100644
index 00000000..bd7e510a
--- /dev/null
+++ b/changelog/984.removal.rst
@@ -0,0 +1,4 @@
+The ``particle`` attribute of `~plasmapy.particles.particle_class.Particle`
+has been deprecated in favor of the new ``symbol`` attribute.  The ``particle``
+attribute now issues a `FutureWarning` to indicate that it will be removed in
+a future release.
diff --git a/changelog/987.breaking.rst b/changelog/987.breaking.rst
new file mode 100644
index 00000000..2bac60a3
--- /dev/null
+++ b/changelog/987.breaking.rst
@@ -0,0 +1,1 @@
+Drop Python 3.6 support.
diff --git a/codemeta.json b/codemeta.json
index 15dfda3d..d68ee0ed 100644
--- a/codemeta.json
+++ b/codemeta.json
@@ -39,7 +39,6 @@
         "science"
     ],
     "programmingLanguage": [
-        "Python 3.6",
         "Python 3.7",
         "Python 3.8",
         "Python 3.9"
@@ -48,9 +47,8 @@
         "OS Independent"
     ],
     "softwareRequirements": [
-        "python >= 3.6",
+        "python >= 3.7",
         "astropy >= 4.0",
-        "colorama >= 0.3",
         "h5py >= 2.8",
         "lmfit >= 1.0.1",
         "matplotlib >= 2.0",
diff --git a/docs/api_static/plasmapy.utils.error_messages.rst b/docs/api_static/plasmapy.utils.error_messages.rst
deleted file mode 100644
index 72fb4647..00000000
--- a/docs/api_static/plasmapy.utils.error_messages.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-:orphan:
-
-`plasmapy.utils.error_messages`
-===============================
-
-.. currentmodule:: plasmapy.utils.error_messages
-
-.. automodapi::  plasmapy.utils.error_messages
-   :include-all-objects:
-   :no-heading:
diff --git a/docs/conf.py b/docs/conf.py
index 1970612c..8cd1a9cc 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3.6
+#!/usr/bin/env python3.7
 # -*- coding: utf-8 -*-
 #
 # PlasmaPy documentation build configuration file, created by
diff --git a/docs/development/code_guide.rst b/docs/development/code_guide.rst
index 5f5593f6..5c02c6d8 100644
--- a/docs/development/code_guide.rst
+++ b/docs/development/code_guide.rst
@@ -7,7 +7,7 @@ Code Development Guidelines
 This document describes the coding requirements and guidelines to be
 followed during the development of PlasmaPy and affiliated packages.
 
-Code written for PlasmaPy must be compatible with Python 3.6 and
+Code written for PlasmaPy must be compatible with Python 3.7 and
 later.
 
 Coding Style
diff --git a/docs/index.rst b/docs/index.rst
index a2638ae7..b62321cf 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -20,7 +20,7 @@ PlasmaPy Documentation
 
 
 `PlasmaPy <http://www.plasmapy.org/>`_ is an open source
-community-developed core `Python <https://www.python.org/>`_ 3.6+
+community-developed core `Python <https://www.python.org/>`_ 3.7+
 package for plasma physics currently under development.
 
 
diff --git a/docs/install.rst b/docs/install.rst
index 4aad2653..3891480e 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -15,13 +15,12 @@ Installing PlasmaPy
 Requirements
 ============
 
-PlasmaPy requires Python version 3.6 or newer.
+PlasmaPy requires Python version 3.7 or newer.
 PlasmaPy requires the following packages for installation:
 
 - `NumPy <http://www.numpy.org/>`_ 1.18.1 or newer
 - `SciPy <https://www.scipy.org/>`_ 1.2 or newer
 - `Astropy <http://www.astropy.org/>`_ 4.0 or newer
-- `colorama <https://pypi.org/project/colorama/>`_ 0.3 or newer
 
 PlasmaPy also depends on the following packages for optional features:
 
diff --git a/docs/particles/decorators.rst b/docs/particles/decorators.rst
index 8f4e806f..5ae98ab3 100644
--- a/docs/particles/decorators.rst
+++ b/docs/particles/decorators.rst
@@ -62,7 +62,7 @@ classes as well:
   class ExampleClass:
       @particle_input
       def particle_symbol(self, particle: Particle) -> str:
-          return particle.particle
+          return particle.symbol
 
 On occasion it is necessary for a function to accept only certain
 categories of particles.  The `~plasmapy.particles.particle_input`
diff --git a/docs/particles/particle_class.rst b/docs/particles/particle_class.rst
index fd437343..2ff5a46e 100644
--- a/docs/particles/particle_class.rst
+++ b/docs/particles/particle_class.rst
@@ -75,12 +75,12 @@ SI units.
 <Quantity 0.49225958 GeV>
 
 Strings representing particles may be accessed using the
-`~plasmapy.particles.Particle.particle`,
+`~plasmapy.particles.Particle.symbol`,
 `~plasmapy.particles.Particle.element`,
 `~plasmapy.particles.Particle.isotope`, and
 `~plasmapy.particles.Particle.ionic_symbol` attributes.
 
->>> antimuon.particle
+>>> antimuon.symbol
 'mu+'
 >>> triton.element
 'H'
diff --git a/docs/utils/index.rst b/docs/utils/index.rst
index 8cace26e..1f329311 100644
--- a/docs/utils/index.rst
+++ b/docs/utils/index.rst
@@ -29,4 +29,5 @@ Reference/API
 
 .. automodapi:: plasmapy.utils.decorators
 .. automodapi:: plasmapy.utils.exceptions
+.. automodapi:: plasmapy.utils.code_repr
 .. automodapi:: plasmapy.utils.pytest_helpers
diff --git a/plasmapy/__init__.py b/plasmapy/__init__.py
index fc3f38bd..bedf9d91 100644
--- a/plasmapy/__init__.py
+++ b/plasmapy/__init__.py
@@ -20,8 +20,8 @@
 # This is the same check as the one at the top of setup.py
 import sys
 
-if sys.version_info < tuple((int(val) for val in "3.6".split("."))):
-    raise Exception("PlasmaPy does not support Python < {}".format(3.6))
+if sys.version_info < (3, 7):
+    raise Exception("PlasmaPy does not support Python < 3.7")
 
 # Packages may add whatever they like to this file, but
 # should keep this content at the top.
diff --git a/plasmapy/formulary/parameters.py b/plasmapy/formulary/parameters.py
index a408026e..d3e128ea 100644
--- a/plasmapy/formulary/parameters.py
+++ b/plasmapy/formulary/parameters.py
@@ -952,7 +952,7 @@ def Hall_parameter(
 
     gyro_frequency = gyrofrequency(B, particle)
     gyro_frequency = gyro_frequency / u.radian
-    if particles.Particle(particle).particle == "e-":
+    if particles.Particle(particle).symbol == "e-":
         coll_rate = fundamental_electron_collision_freq(
             T, n, ion, coulomb_log, V, coulomb_log_method=coulomb_log_method
         )
diff --git a/plasmapy/particles/ionization_state.py b/plasmapy/particles/ionization_state.py
index bfd385cf..61976b50 100644
--- a/plasmapy/particles/ionization_state.py
+++ b/plasmapy/particles/ionization_state.py
@@ -286,7 +286,7 @@ def __init__(
 
         except Exception as exc:
             raise ParticleError(
-                f"Unable to create IonizationState object for {particle.particle}."
+                f"Unable to create IonizationState object for {particle.symbol}."
             ) from exc
 
     def __str__(self) -> str:
@@ -649,8 +649,7 @@ def _particle_instances(self) -> List[Particle]:
         instances corresponding to each ion.
         """
         return [
-            Particle(self._particle.particle, Z=i)
-            for i in range(self.atomic_number + 1)
+            Particle(self._particle.symbol, Z=i) for i in range(self.atomic_number + 1)
         ]
 
     @property
diff --git a/plasmapy/particles/ionization_state_collection.py b/plasmapy/particles/ionization_state_collection.py
index c7c03b99..4b582864 100644
--- a/plasmapy/particles/ionization_state_collection.py
+++ b/plasmapy/particles/ionization_state_collection.py
@@ -542,7 +542,7 @@ def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
                 n_elems = {}
 
             for key in sorted_keys:
-                new_key = particles[key].particle
+                new_key = particles[key].symbol
                 _particle_instances.append(particles[key])
                 if new_key in _elements_and_isotopes:
                     raise ParticleError(
@@ -572,10 +572,10 @@ def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
                     isinstance(inputs[key], np.ndarray)
                     and inputs[key].dtype.kind == "f"
                 ):
-                    new_ionic_fractions[particles[key].particle] = inputs[key]
+                    new_ionic_fractions[particles[key].symbol] = inputs[key]
                 else:
                     try:
-                        new_ionic_fractions[particles[key].particle] = np.array(
+                        new_ionic_fractions[particles[key].symbol] = np.array(
                             inputs[key], dtype=np.float
                         )
                     except ValueError as exc:
@@ -632,10 +632,10 @@ def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
                 key=lambda p: (p.atomic_number, p.mass_number if p.isotope else 0)
             )
             _elements_and_isotopes = [
-                particle.particle for particle in _particle_instances
+                particle.symbol for particle in _particle_instances
             ]
             new_ionic_fractions = {
-                particle.particle: np.full(
+                particle.symbol: np.full(
                     particle.atomic_number + 1, fill_value=np.nan, dtype=np.float64
                 )
                 for particle in _particle_instances
diff --git a/plasmapy/particles/parsing.py b/plasmapy/particles/parsing.py
index 3b813099..191b149f 100644
--- a/plasmapy/particles/parsing.py
+++ b/plasmapy/particles/parsing.py
@@ -452,7 +452,7 @@ def _reconstruct_ion_symbol(
         symbol = element
 
     nomenclature_dict = {
-        "particle": symbol,
+        "symbol": symbol,
         "element": element,
         "isotope": isotope,
         "ion": ion,
diff --git a/plasmapy/particles/particle_class.py b/plasmapy/particles/particle_class.py
index e0c0f241..0db349e9 100644
--- a/plasmapy/particles/particle_class.py
+++ b/plasmapy/particles/particle_class.py
@@ -138,7 +138,7 @@ def json_dict(self) -> dict:
                 # string representation of the module contains the particle class
                 "module": "plasmapy.particles.particle_class",
 
-                # date stamp of when the object was creaed
+                # date stamp of when the object was created
                 "date_created": "2020-07-20 17:46:13 UTC",
 
                 # parameters used to initialized the particle class
@@ -163,6 +163,11 @@ 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
@@ -261,10 +266,10 @@ class Particle(AbstractParticle):
     >>> positron = Particle('positron')
     >>> hydrogen = Particle(1)  # atomic number
 
-    The `~plasmapy.particles.Particle.particle` attribute returns the
+    The `~plasmapy.particles.Particle.symbol` attribute returns the
     particle's symbol in the standard form.
 
-    >>> positron.particle
+    >>> positron.symbol
     'e+'
 
     The ``element``, ``isotope``, and ``ionic_symbol`` attributes return
@@ -392,7 +397,7 @@ def __init__(
         # copy).
 
         if isinstance(argument, Particle):
-            argument = argument.particle
+            argument = argument.symbol
 
         if mass_numb is not None and not isinstance(mass_numb, Integral):
             raise TypeError("mass_numb is not an integer")
@@ -413,30 +418,30 @@ def __init__(
         # case-insensitive aliases for particles, return the standard
         # symbol. Otherwise, return the original argument.
 
-        particle = _dealias_particle_aliases(argument)
+        particle_symbol = _dealias_particle_aliases(argument)
 
-        if particle in _Particles.keys():  # special particles
+        if particle_symbol in _Particles.keys():  # special particles
 
-            attributes["particle"] = particle
+            attributes["symbol"] = particle_symbol
 
-            for attribute in _Particles[particle].keys():
-                attributes[attribute] = _Particles[particle][attribute]
+            for attribute in _Particles[particle_symbol].keys():
+                attributes[attribute] = _Particles[particle_symbol][attribute]
 
             particle_taxonomy = ParticleZoo._taxonomy_dict
             all_categories = particle_taxonomy.keys()
 
             for category in all_categories:
-                if particle in particle_taxonomy[category]:
+                if particle_symbol in particle_taxonomy[category]:
                     categories.add(category)
 
             if attributes["name"] in _specific_particle_categories:
                 categories.add(attributes["name"])
 
-            if particle == "p+":
+            if particle_symbol == "p+":
                 categories.update({"element", "isotope", "ion"})
 
             if mass_numb is not None or Z is not None:
-                if particle == "p+" and (mass_numb == 1 or Z == 1):
+                if particle_symbol == "p+" and (mass_numb == 1 or Z == 1):
                     warnings.warn(
                         "Redundant mass number or charge information.", ParticleWarning
                     )
@@ -542,11 +547,11 @@ def __repr__(self) -> str:
         >>> repr(lead)
         'Particle("Pb")'
         """
-        return f'Particle("{self.particle}")'
+        return f'Particle("{self.symbol}")'
 
     def __str__(self) -> str:
         """Return the particle's symbol."""
-        return self.particle
+        return self.symbol
 
     def __eq__(self, other) -> bool:
         """
@@ -560,7 +565,7 @@ def __eq__(self, other) -> bool:
 
         If ``other`` is not a `str` or `~plasmapy.particles.Particle`
         instance, then this method will raise a `TypeError`.  If
-        ``other.particle`` equals ``self.particle`` but the attributes
+        ``other.symbol`` equals ``self.symbol`` but the attributes
         differ, then this method will raise a
         `~plasmapy.particles.exceptions.ParticleError`.
 
@@ -576,7 +581,7 @@ def __eq__(self, other) -> bool:
         if isinstance(other, str):
             try:
                 other_particle = Particle(other)
-                return self.particle == other_particle.particle
+                return self.symbol == other_particle.symbol
             except InvalidParticleError as exc:
                 raise InvalidParticleError(
                     f"{other} is not a particle and cannot be compared to {self}."
@@ -587,15 +592,15 @@ def __eq__(self, other) -> bool:
                 f"The equality of a Particle object with a {type(other)} is undefined."
             )
 
-        no_particle_attr = "particle" not in dir(self) or "particle" not in dir(other)
+        no_symbol_attr = "symbol" not in dir(self) or "symbol" not in dir(other)
         no_attributes_attr = "_attributes" not in dir(self) or "_attributes" not in dir(
             other
         )
 
-        if no_particle_attr or no_attributes_attr:  # coverage: ignore
+        if no_symbol_attr or no_attributes_attr:  # coverage: ignore
             raise TypeError(f"The equality of {self} with {other} is undefined.")
 
-        same_particle = self.particle == other.particle
+        same_particle = self.symbol == other.symbol
 
         # The following two loops are a hack to enable comparisons
         # between defaultdicts.  By accessing all of the defined keys in
@@ -635,7 +640,7 @@ def __ne__(self, other) -> bool:
 
         If ``other`` is not a `str` or `~plasmapy.particles.Particle`
         instance, then this method will raise a `TypeError`.  If
-        ``other.particle`` equals ``self.particle`` but the attributes
+        ``other.symbol`` equals ``self.symbol`` but the attributes
         differ, then this method will raise a
         `~plasmapy.particles.exceptions.ParticleError`.
         """
@@ -678,21 +683,47 @@ def json_dict(self) -> dict:
             '__init__': {'args': ('e-',), 'kwargs': {}}}}
         """
         particle_dictionary = super().json_dict
-        particle_dictionary["plasmapy_particle"]["__init__"]["args"] = (self.particle,)
+        particle_dictionary["plasmapy_particle"]["__init__"]["args"] = (self.symbol,)
         return particle_dictionary
 
     @property
-    def particle(self) -> Optional[str]:
+    def particle(self) -> str:
+        """
+        Return the symbol of the particle, atom, isotope, or ion.
+
+        .. deprecated:: 0.6.0
+            `Particle.particle` has been deprecated and will be removed in
+            a subsequent release of PlasmaPy.  Use `Particle.symbol`
+            instead.
+        """
+        warnings.warn(
+            "Particle.particle has been deprecated and will be removed in "
+            "a subsequent release. Use Particle.symbol instead.",
+            FutureWarning,
+        )
+        return self.symbol
+
+    @property
+    def symbol(self) -> str:
         """
-        Return the particle's symbol.
+        Return the symbol of the particle, atom, isotope, or ion.
+
+        This attribute will return the canonical symbol for special
+        particles (e.g., ``"p+"``, ``"e-"``, or ``"n"``), the atomic
+        symbol for elements (e.g., ``"Fe"``), the isotopic symbol for
+        isotopes (e.g., ``"D"`` or ``"Fe-56"``), and the ionic symbol
+        for ions (e.g., ``"N 1+"`` or ``"He-4 1+"``).
 
         Examples
         --------
-        >>> electron = Particle('electron')
-        >>> electron.particle
-        'e-'
+        >>> electron = Particle('positron')
+        >>> electron.symbol
+        'e+'
+        >>> deuteron = Particle('D 1+')
+        >>> deuteron.symbol
+        'D 1+'
         """
-        return self._attributes["particle"]
+        return self._attributes["symbol"]
 
     @property
     def antiparticle(self):
@@ -714,8 +745,8 @@ def antiparticle(self):
         >>> ~antineutron
         Particle("n")
         """
-        if self.particle in _antiparticles.keys():
-            return Particle(_antiparticles[self.particle])
+        if self.symbol in _antiparticles.keys():
+            return Particle(_antiparticles[self.symbol])
         else:
             raise ParticleError(
                 "The unary operator can only be used for elementary "
@@ -837,7 +868,7 @@ def isotope_name(self) -> str:
         'iron-56'
         """
         if not self.element:
-            raise InvalidElementError(_category_errmsg(self.particle, "element"))
+            raise InvalidElementError(_category_errmsg(self.symbol, "element"))
         elif not self.isotope:
             raise InvalidIsotopeError(_category_errmsg(self, "isotope"))
 
@@ -915,49 +946,6 @@ def standard_atomic_weight(self) -> u.Quantity:
             )
         return self._attributes["standard atomic weight"].to(u.kg)
 
-    @property
-    def nuclide_mass(self) -> u.Quantity:
-        """
-        Return the mass of the bare nucleus of an isotope or a neutron.
-
-        This attribute will raise a
-        `~plasmapy.particles.exceptions.InvalidIsotopeError` if the particle is not an
-        isotope or neutron, or a
-        `~plasmapy.particles.exceptions.MissingParticleDataError` if the isotope mass is
-        not available.
-
-        Examples
-        --------
-        >>> deuterium = Particle('D')
-        >>> deuterium.nuclide_mass
-        <Quantity 3.34358372e-27 kg>
-        """
-
-        if self.isotope == "H-1":
-            return const.m_p
-        elif self.isotope == "D":
-            return _special_ion_masses["D 1+"]
-        elif self.isotope == "T":
-            return _special_ion_masses["T 1+"]
-        elif self.particle == "n":
-            return const.m_n
-
-        if not self.isotope:
-            raise InvalidIsotopeError(_category_errmsg(self, "isotope"))
-
-        base_mass = self._attributes["isotope mass"]
-
-        if base_mass is None:  # coverage: ignore
-            raise MissingParticleDataError(
-                f"The mass of a {self.isotope} nuclide is not available."
-            )
-
-        _nuclide_mass = (
-            self._attributes["isotope mass"] - self.atomic_number * const.m_e
-        )
-
-        return _nuclide_mass.to(u.kg)
-
     @property
     def mass(self) -> u.Quantity:
         """
@@ -1049,7 +1037,7 @@ def nuclide_mass(self) -> u.Quantity:
             return _special_ion_masses["D 1+"]
         elif self.isotope == "T":
             return _special_ion_masses["T 1+"]
-        elif self.particle == "n":
+        elif self.symbol == "n":
             return const.m_n
 
         if not self.isotope:
@@ -1099,7 +1087,7 @@ def mass_energy(self) -> u.Quantity:
             return energy.to(u.J)
         except MissingParticleDataError:
             raise MissingParticleDataError(
-                f"The mass energy of {self.particle} is not available "
+                f"The mass energy of {self.symbol} is not available "
                 f"because the mass is unknown."
             ) from None
 
@@ -1212,7 +1200,7 @@ def neutron_number(self) -> Integral:
         >>> Particle('n').neutron_number
         1
         """
-        if self.particle == "n":
+        if self.symbol == "n":
             return 1
         elif self.isotope:
             return self.mass_number - self.atomic_number
@@ -1237,7 +1225,7 @@ def electron_number(self) -> Integral:
         >>> Particle('e-').electron_number
         1
         """
-        if self.particle == "e-":
+        if self.symbol == "e-":
             return 1
         elif self.ionic_symbol:
             return self.atomic_number - self.integer_charge
@@ -1263,7 +1251,7 @@ def isotopic_abundance(self) -> u.Quantity:
         from .atomic import common_isotopes
 
         if not self.isotope or self.is_ion:  # coverage: ignore
-            raise InvalidIsotopeError(_category_errmsg(self.particle, "isotope"))
+            raise InvalidIsotopeError(_category_errmsg(self.symbol, "isotope"))
 
         abundance = self._attributes.get("isotopic abundance", 0.0)
 
@@ -1296,7 +1284,7 @@ def baryon_number(self) -> Integral:
         """
         if self._attributes["baryon number"] is None:  # coverage: ignore
             raise MissingParticleDataError(
-                f"The baryon number for '{self.particle}' is not available."
+                f"The baryon number for '{self.symbol}' is not available."
             )
         return self._attributes["baryon number"]
 
@@ -1323,7 +1311,7 @@ def lepton_number(self) -> Integral:
         """
         if self._attributes["lepton number"] is None:  # coverage: ignore
             raise MissingParticleDataError(
-                f"The lepton number for {self.particle} is not available."
+                f"The lepton number for {self.symbol} is not available."
             )
         return self._attributes["lepton number"]
 
@@ -1345,18 +1333,18 @@ def half_life(self) -> Union[u.Quantity, str]:
         <Quantity 881.5 s>
         """
         if self.element and not self.isotope:
-            raise InvalidIsotopeError(_category_errmsg(self.particle, "isotope"))
+            raise InvalidIsotopeError(_category_errmsg(self.symbol, "isotope"))
 
         if isinstance(self._attributes["half-life"], str):
             warnings.warn(
-                f"The half-life for {self.particle} is not known precisely; "
+                f"The half-life for {self.symbol} is not known precisely; "
                 "returning string with estimated value.",
                 MissingParticleDataWarning,
             )
 
         if self._attributes["half-life"] is None:
             raise MissingParticleDataError(
-                f"The half-life of '{self.particle}' is not available."
+                f"The half-life of '{self.symbol}' is not available."
             )
         return self._attributes["half-life"]
 
@@ -1376,7 +1364,7 @@ def spin(self) -> Real:
         """
         if self._attributes["spin"] is None:
             raise MissingParticleDataError(
-                f"The spin of particle '{self.particle}' is unavailable."
+                f"The spin of particle '{self.symbol}' is unavailable."
             )
 
         return self._attributes["spin"]
@@ -1405,7 +1393,7 @@ def periodic_table(self) -> namedtuple:
         if self.element:
             return self._attributes["periodic table"]
         else:  # coverage: ignore
-            raise InvalidElementError(_category_errmsg(self.particle, "element"))
+            raise InvalidElementError(_category_errmsg(self.symbol, "element"))
 
     @property
     def categories(self) -> Set[str]:
@@ -1606,17 +1594,16 @@ def ionize(self, n: Integral = 1, inplace: bool = False):
         """
         if not self.element:
             raise InvalidElementError(
-                f"Cannot ionize {self.particle} because it is not a "
+                f"Cannot ionize {self.symbol} because it is not a "
                 f"neutral atom or ion."
             )
         if not self.is_category(any_of={"charged", "uncharged"}):
             raise ChargeError(
-                f"Cannot ionize {self.particle} because its charge "
-                f"is not specified."
+                f"Cannot ionize {self.symbol} because its charge " f"is not specified."
             )
         if self.integer_charge == self.atomic_number:
             raise InvalidIonError(
-                f"The particle {self.particle} is already fully "
+                f"The particle {self.symbol} is already fully "
                 f"ionized and cannot be ionized further."
             )
         if not isinstance(n, Integral):
@@ -1690,12 +1677,12 @@ def recombine(self, n: Integral = 1, inplace=False):
 
         if not self.element:
             raise InvalidElementError(
-                f"{self.particle} cannot undergo recombination because "
+                f"{self.symbol} cannot undergo recombination because "
                 f"it is not a neutral atom or ion."
             )
         if not self.is_category(any_of={"charged", "uncharged"}):
             raise ChargeError(
-                f"{self.particle} cannot undergo recombination because "
+                f"{self.symbol} cannot undergo recombination because "
                 f"its charge is not specified."
             )
         if not isinstance(n, Integral):
@@ -1955,47 +1942,11 @@ def json_dict(self) -> dict:
         }
         return particle_dictionary
 
-    @property
-    def mass(self) -> u.kg:
-        """Return the custom particle's mass."""
-        return self._mass
-
     @property
     def charge(self) -> u.C:
         """Return the custom particle's electric charge in coulombs."""
         return self._charge
 
-    @mass.setter
-    def mass(self, m: u.kg):
-        if m is None:
-            m = np.nan * u.kg
-            warnings.warn(
-                "CustomParticle mass set to NaN kg", MissingParticleDataWarning
-            )
-        elif isinstance(m, str):
-            m = u.Quantity(m)
-        elif not isinstance(m, u.Quantity):
-            raise TypeError(
-                "The mass of a custom particle must be a nonnegative Quantity "
-                "with units of mass."
-            )
-        if np.isnan(m):
-            self._mass = m
-        else:
-            if not isinstance(m.value, Real):
-                raise TypeError(
-                    "The mass of a custom particle must be a real number "
-                    "with units of mass."
-                )
-            try:
-                self._mass = m.to(u.kg)
-                if self.mass < 0 * u.kg:
-                    raise ValueError("The mass of a particle must be nonnegative.")
-            except u.UnitsError as exc:
-                raise u.UnitsError(
-                    "The mass of a custom particle must have units of mass."
-                ) from exc
-
     @charge.setter
     def charge(self, q: Optional[Union[u.Quantity, Real]]):
         if q is None:
@@ -2035,6 +1986,42 @@ def charge(self, q: Optional[Union[u.Quantity, Real]]):
                 "the elementary charge."
             )
 
+    @property
+    def mass(self) -> u.kg:
+        """Return the custom particle's mass."""
+        return self._mass
+
+    @mass.setter
+    def mass(self, m: u.kg):
+        if m is None:
+            m = np.nan * u.kg
+            warnings.warn(
+                "CustomParticle mass set to NaN kg", MissingParticleDataWarning
+            )
+        elif isinstance(m, str):
+            m = u.Quantity(m)
+        elif not isinstance(m, u.Quantity):
+            raise TypeError(
+                "The mass of a custom particle must be a nonnegative Quantity "
+                "with units of mass."
+            )
+        if np.isnan(m):
+            self._mass = m
+        else:
+            if not isinstance(m.value, Real):
+                raise TypeError(
+                    "The mass of a custom particle must be a real number "
+                    "with units of mass."
+                )
+            try:
+                self._mass = m.to(u.kg)
+                if self.mass < 0 * u.kg:
+                    raise ValueError("The mass of a particle must be nonnegative.")
+            except u.UnitsError as exc:
+                raise u.UnitsError(
+                    "The mass of a custom particle must have units of mass."
+                ) from exc
+
 
 # TODO: Describe valid particle representations in docstring of particle_like
 
diff --git a/plasmapy/particles/symbols.py b/plasmapy/particles/symbols.py
index d9763c30..eaff6fbc 100644
--- a/plasmapy/particles/symbols.py
+++ b/plasmapy/particles/symbols.py
@@ -284,7 +284,7 @@ def particle_symbol(
     >>> particle_symbol('H-1', Z=-1)
     'H-1 1-'
     """
-    return particle.particle
+    return particle.symbol
 
 
 @particle_input
diff --git a/plasmapy/plasma/sources/plasmablob.py b/plasmapy/plasma/sources/plasmablob.py
index a02aa65e..cbd25e35 100644
--- a/plasmapy/plasma/sources/plasmablob.py
+++ b/plasmapy/plasma/sources/plasmablob.py
@@ -11,8 +11,9 @@
 from plasmapy.formulary.parameters import _grab_charge
 from plasmapy.particles import particle_mass
 from plasmapy.plasma.plasma_base import GenericPlasma
-from plasmapy.utils import call_string, CouplingWarning
+from plasmapy.utils import code_repr
 from plasmapy.utils.decorators import validate_quantities
+from plasmapy.utils.exceptions import CouplingWarning
 
 
 class PlasmaBlob(GenericPlasma):
@@ -69,7 +70,7 @@ def __repr__(self):
             "Z": self.Z,
         }
 
-        return call_string(PlasmaBlob, (), argument_dict)
+        return code_repr.call_string(PlasmaBlob, (), argument_dict)
 
     @property
     def electron_temperature(self):
diff --git a/plasmapy/utils/__init__.py b/plasmapy/utils/__init__.py
index 535734af..7d14001f 100644
--- a/plasmapy/utils/__init__.py
+++ b/plasmapy/utils/__init__.py
@@ -10,20 +10,18 @@
     "PlasmaPyWarning",
     "RelativityError",
     "RelativityWarning",
-    "call_string",
     "decorators",
     "exceptions",
     "roman",
 ]
 
 from plasmapy.utils import (
+    code_repr,
     datatype_factory_base,
     decorators,
-    error_messages,
     exceptions,
     roman,
 )
-from plasmapy.utils.error_messages import call_string
 from plasmapy.utils.exceptions import (
     CouplingWarning,
     PhysicsError,
@@ -37,6 +35,6 @@
 try:
     from plasmapy.utils import pytest_helpers
 except ModuleNotFoundError:
-    # pytest is not a hard dependency, so only import pytest_helpers is pytest
+    # pytest is not a hard dependency, so only import pytest_helpers if pytest
     # is installed
     pass
diff --git a/plasmapy/utils/code_repr.py b/plasmapy/utils/code_repr.py
new file mode 100644
index 00000000..b5ed9133
--- /dev/null
+++ b/plasmapy/utils/code_repr.py
@@ -0,0 +1,412 @@
+"""Tools for formatting strings, including for error messages."""
+
+__all__ = ["call_string", "attribute_call_string", "method_call_string"]
+
+import inspect
+import numpy as np
+
+from astropy import units as u
+from numbers import Integral
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+
+def _code_repr_of_ndarray(array: np.ndarray, max_items=np.inf) -> str:
+    """
+    Transform a `~numpy.ndarray` into a format as would appear in a
+    function call, after having done ``import numpy as np``.
+
+    If ``max_items`` is less than ``array.size``, then include only the
+    first ``max_items`` elements of the array in the resulting string
+    and replace the remaining items with ``"..."``.
+    """
+
+    def remove_excess_spaces(s: str) -> str:
+        s = " ".join(s.split())
+        s = s.replace(" ,", ",")
+        s = s.replace("[ ", "[")
+        return s
+
+    def put_np_before_infs_and_nans(s: str) -> str:
+        s = s.replace("inf", "np.inf")
+        s = s.replace("nan", "np.nan")
+        return s
+
+    def replace_excess_items_with_ellipsis(s: str, max_items: Integral):
+        substrings_between_commas = s.split(",")
+        to_comma_before_ellipsis = ",".join(substrings_between_commas[0:max_items])
+        closing_brackets = "]" * substrings_between_commas[-1].count("]")
+        closing = f", ...{closing_brackets})"
+        return to_comma_before_ellipsis + closing
+
+    s = np.array_repr(array, max_line_width=np.inf, suppress_small=False)
+    s = remove_excess_spaces(s)
+    s = put_np_before_infs_and_nans(s)
+
+    if array.size > max_items:
+        s = replace_excess_items_with_ellipsis(s, max_items)
+
+    return f"np.{s}"
+
+
+def _code_repr_of_quantity(arg: u.Quantity, max_items=np.inf) -> str:
+    """
+    Transform a `~astropy.units.Quantity` into a format as would appear
+    in a function call.
+    """
+    if isinstance(arg.value, np.ndarray):
+        formatted = _code_repr_of_ndarray(arg.value, max_items=max_items)
+    else:
+        formatted = repr(arg.value)
+
+    if arg.unit == u.dimensionless_unscaled:
+        formatted += "*u.dimensionless_unscaled"
+    else:
+        for base, power in zip(arg.unit.bases, arg.unit.powers):
+            if power == -1:
+                formatted += f"/u.{base}"
+            elif power == 1:
+                formatted += f"*u.{base}"
+            else:
+                formatted += f"*u.{base}**{power}"
+
+    return formatted
+
+
+def _code_repr_of_arg(arg, max_items=np.inf) -> str:
+    """Transform an argument into a format as would appear in a function call."""
+    if hasattr(arg, "__name__"):
+        return arg.__name__
+    function_for_type = {
+        u.Quantity: _code_repr_of_quantity,
+        np.ndarray: _code_repr_of_ndarray,
+    }
+    for arg_type in function_for_type.keys():
+        if isinstance(arg, arg_type):
+            code_repr_func = function_for_type[arg_type]
+            return code_repr_func(arg, max_items=max_items)
+    return repr(arg)
+
+
+def _code_repr_of_args_and_kwargs(
+    args: Any = None, kwargs: Dict = None, max_items=np.inf
+) -> str:
+    """
+    Take positional and keyword arguments, and format them into a
+    string as they would appear in a function call (excluding parentheses).
+    """
+    args = tuple() if args is None else args
+    kwargs = dict() if kwargs is None else kwargs
+
+    args_collection = args if isinstance(args, (tuple, list)) else (args,)
+
+    args_and_kwargs = ""
+
+    for arg in args_collection:
+        args_and_kwargs += f"{_code_repr_of_arg(arg, max_items)}, "
+
+    for kw_name, kw_val in kwargs.items():
+        args_and_kwargs += f"{kw_name}={_code_repr_of_arg(kw_val, max_items)}, "
+
+    if args_and_kwargs[-2:] == ", ":
+        args_and_kwargs = args_and_kwargs[:-2]
+
+    return args_and_kwargs
+
+
+def _name_with_article(ex: Exception) -> str:
+    """
+    Return a string with an indefinite article and the name of
+    the exception ``ex`` (e.g. ``Exception`` -> ``'an Exception'``).
+
+    Notes
+    -----
+    If this function is to be expanded for cases beyond exceptions,
+    we would need to either expand the treatment of cases that do not
+    follow the general rule, or use a library like `inflect` on PyPI.
+    """
+    starts_with_vowel_but_uses_a = ["use", "uni"]
+    name = ex.__name__
+    use_an = all(
+        [
+            name[0] in "aeiouAEIOU",
+            name[0:3].lower() not in starts_with_vowel_but_uses_a,
+        ]
+    )
+    indefinite_article = "an" if use_an else "a"
+    return f"{indefinite_article} {name}"
+
+
+def _object_name(obj: Any, showmodule=False) -> str:
+    """
+    Return the name of an `object`.  If the `object` has a `__name__`
+    attribute and ``showmodule`` is `True`, then prepend the module
+    name if not in `builtins`.  Replace ``"numpy"`` with ``"np"`` and
+    substitute ``"u"`` for several modules in `astropy.units`.
+    """
+
+    def substitute_module_shortcuts(module_name):
+        """Substitute common import shortcuts within module names."""
+        replacements = {
+            "numpy": "np",
+            "builtins": "",
+            "astropy.units.core": "u",
+            "astropy.units.quantity": "u",
+            "astropy.units.decorators": "u",
+            "astropy.units": "u",
+        }
+        for old, new in replacements.items():
+            if module_name.startswith(old):
+                module_name = module_name.replace(old, new, 1)
+        return module_name
+
+    obj_name = obj.__name__ if hasattr(obj, "__name__") else repr(obj)
+    module_name = inspect.getmodule(obj).__name__ if showmodule else ""
+    module_name = substitute_module_shortcuts(module_name)
+    if module_name:
+        obj_name = f"{module_name}.{obj_name}"
+    return obj_name
+
+
+def _string_together_warnings_for_printing(
+    warning_types: List[Warning], warning_messages: List[str]
+):
+    """
+    Take a list of warning types with a list of corresponding warning
+    messages, and create a string that prints out each warning type
+    followed by the corresponding message, separated by a full line.
+    """
+    warnings_with_messages = [
+        _object_name(warning, showmodule=False) + ": " + message
+        for warning, message in zip(warning_types, warning_messages)
+    ]
+    return "\n\n".join(warnings_with_messages)
+
+
+def call_string(
+    f: Callable,
+    args: Any = None,
+    kwargs: Dict[str, Any] = None,
+    max_items: Integral = 12,
+) -> str:
+    """
+    Approximate a call of a function or class with positional and
+    keyword arguments.
+
+    Parameters
+    ----------
+    f : callable
+        A function, class, or other callable object
+
+    args : `tuple`, `list`, or any; optional
+        A `tuple` or `list` containing positional arguments, or any
+        other `object` if there is only one positional argument
+
+    kwargs : `dict`, optional
+        A `dict` containing keyword arguments
+
+    max_items : int, optional
+        The maximum number of items to include in a `~numpy.ndarray` or
+        `~astropy.Quantity`; additional items will be truncated with an
+        ellipsis.  Defaults to 12.
+
+    Returns
+    -------
+    str
+        Approximation to a call of ``f`` with ``args`` as positional
+        arguments and ``kwargs`` as keyword arguments.
+
+    See Also
+    --------
+    attribute_call_string
+    method_call_string
+
+    Notes
+    -----
+    This function will generally provide an exact call string for most
+    common types of simple positional and keyword arguments.  When
+    dealing with types that are not accounted for, this function will
+    fall back on `repr`.
+
+    This function assumes aliases of ``u`` for `astropy.units` and ``np``
+    for `numpy`.
+
+    Examples
+    --------
+    >>> call_string(int, 3.14159)
+    'int(3.14159)'
+    >>> call_string(int, args=(9.2,), kwargs={'base': 2})
+    'int(9.2, base=2)'
+    """
+    args = tuple() if args is None else args
+    kwargs = dict() if kwargs is None else kwargs
+    args_and_kwargs = _code_repr_of_args_and_kwargs(args, kwargs, max_items)
+    return f"{f.__name__}({args_and_kwargs})"
+
+
+def attribute_call_string(
+    cls,
+    attr: str,
+    args_to_cls: Optional[Union[Tuple, Any]] = None,
+    kwargs_to_cls: Optional[Dict[str, Any]] = None,
+    max_items: Integral = 12,
+) -> str:
+    """
+    Approximate the command to instantiate a class, and access an
+    attribute of the resulting class instance.
+
+    Parameters
+    ----------
+    cls : `class`
+        The class to be used in the string representation
+
+    attr: `str`
+        The name of the attribute of class ``cls``
+
+    args_to_cls : `tuple`, `list`, or any; optional
+        A `tuple` or `list` containing positional arguments, or any
+        other type of `object` if there is only one positional argument,
+        to be used during instantiation of ``cls``
+
+    kwargs_to_cls: `dict`, optional
+        A `dict` containing the keyword arguments to be used during
+        instantiation of ``cls``
+
+    max_items : int, optional
+        The maximum number of items to include in a `~numpy.ndarray` or
+        `~astropy.Quantity`; additional items will be truncated with an
+        ellipsis.  Defaults to 12.
+
+    Returns
+    -------
+    str
+        Approximation of a command to instantiate ``cls`` with
+        ``args_to_cls`` as positional arguments and ``kwargs_to_cls`` as
+        keyword arguments, and then access the attribute ``attr``.
+
+    See Also
+    --------
+    attribute_call_string
+    method_call_string
+
+    Notes
+    -----
+    This function will generally provide an exact call string for most
+    common types of simple positional and keyword arguments.  When
+    dealing with types that are not accounted for, this function will
+    fall back on `repr`.
+
+    This function assumes aliases of ``u`` for `astropy.units` and ``np``
+    for `numpy`.
+
+    Examples
+    --------
+    >>> class SampleClass:
+    ...     def __init__(self, arg1, kwarg1=None):
+    ...         pass
+    ...     @property
+    ...     def attribute(self):
+    ...         return 42
+    >>> args_to_cls = (1, 2)
+    >>> kwargs_to_cls = {'kwarg1': 2}
+    >>> attribute_call_string(SampleClass, 'attribute', args_to_cls, kwargs_to_cls)
+    'SampleClass(1, 2, kwarg1=2).attribute'
+    """
+    args_to_cls = tuple() if args_to_cls is None else args_to_cls
+    kwargs_to_cls = dict() if kwargs_to_cls is None else kwargs_to_cls
+    return f"{call_string(cls, args_to_cls, kwargs_to_cls, max_items)}.{attr}"
+
+
+def method_call_string(
+    cls,
+    method: str,
+    *,
+    args_to_cls: Optional[Any] = None,
+    kwargs_to_cls: Optional[Dict[str, Any]] = None,
+    args_to_method: Optional[Any] = None,
+    kwargs_to_method: Optional[Dict[str, Any]] = None,
+    max_items: Integral = 12,
+) -> str:
+    """
+    Approximate the command to instantiate a class, and then call a
+    method in the resulting class instance.
+
+    Parameters
+    ----------
+    cls : `class`
+        The class to be used in the string representation
+
+    method: `str`
+        The name of the method in class ``cls``
+
+    args_to_cls : `tuple`, `list`, or any; keyword-only, optional
+        A `tuple` or `list` containing positional arguments, or any
+        other type of `object` if there is only one positional argument,
+        to be used during instantiation of ``cls``
+
+    kwargs_to_cls: `dict`, keyword-only optional
+        A `dict` containing the keyword arguments to be used during
+        instantiation of ``cls``
+
+    args_to_method : `tuple`, `list`, or any; keyword-only, optional
+        A `tuple` or `list` containing the positional arguments to be
+        used in the method call, or any other `object` if there is only
+        one positional argument
+
+    kwargs_to_method: `dict`, keyword-only, optional
+        A `dict` containing the keyword arguments to be used during
+        the method call
+
+    max_items : int, keyword-only, optional
+        The maximum number of items to include in a `~numpy.ndarray` or
+        `~astropy.Quantity`; additional items will be truncated with an
+        ellipsis.  Defaults to 12.
+
+    Returns
+    -------
+    str
+        Approximation of a command to instantiate ``cls`` with
+        ``args_to_cls`` as positional arguments and ``kwargs_to_cls`` as
+        keyword arguments, and then call a method of the resulting
+        instance with ``args_to_method`` as positional arguments and
+        ``kwargs_to_method`` as keyword arguments.
+
+    See Also
+    --------
+    call_string
+    attribute_call_string
+
+    Notes
+    -----
+    This function will generally provide an exact call string for most
+    common types of simple positional and keyword arguments.  When
+    dealing with types that are not accounted for, this function will
+    fall back on `repr`.
+
+    This function assumes aliases of ``u`` for `astropy.units` and ``np``
+    for `numpy`.
+
+    Examples
+    --------
+    >>> class SampleClass:
+    ...     def __init__(self, cls_arg, cls_kwarg=None):
+    ...         pass
+    ...     def method(self, method_arg, method_kwarg=None):
+    ...         return 42
+    >>> c_args = (1,)
+    >>> c_kwargs = {'cls_kwarg': 2}
+    >>> m_args = 3
+    >>> m_kwargs = {'method_kwarg': 4}
+    >>> method_call_string(
+    ...     SampleClass,
+    ...     'method',
+    ...     args_to_cls=c_args,
+    ...     kwargs_to_cls=c_kwargs,
+    ...     args_to_method=m_args,
+    ...     kwargs_to_method=m_kwargs)
+    'SampleClass(1, cls_kwarg=2).method(3, method_kwarg=4)'
+    """
+    class_call_string = f"{call_string(cls, args_to_cls, kwargs_to_cls, max_items)}"
+    args_to_method_and_kwargs = _code_repr_of_args_and_kwargs(
+        args_to_method, kwargs_to_method
+    )
+    return f"{class_call_string}.{method}({args_to_method_and_kwargs})"
diff --git a/plasmapy/utils/error_messages.py b/plasmapy/utils/error_messages.py
deleted file mode 100644
index abdac69c..00000000
--- a/plasmapy/utils/error_messages.py
+++ /dev/null
@@ -1,102 +0,0 @@
-__all__ = ["call_string"]
-
-import astropy.units as u
-import colorama
-
-from typing import Any, Callable, Dict
-
-_bold = colorama.Style.BRIGHT
-_magenta = colorama.Fore.MAGENTA
-_blue = colorama.Fore.BLUE
-_cyan = colorama.Fore.CYAN
-_red = colorama.Fore.RED
-
-_exception_color = f"{_magenta}{_bold}"
-_type_color = f"{_magenta}{_bold}"
-_func_color = f"{_cyan}{_bold}"
-_result_color = f"{_blue}{_bold}"
-_message_color = f"{_red}{_bold}"
-
-
-def _format_quantity(arg):
-    formatted = f"{arg.value}"
-    for base, power in zip(arg.unit.bases, arg.unit.powers):
-        if power == -1:
-            formatted += f"/u.{base}"
-        elif power == 1:
-            formatted += f"*u.{base}"
-        else:
-            formatted += f"*u.{base}**{power}"
-    return formatted
-
-
-def _format_arg(arg):
-    if hasattr(arg, "__name__"):
-        return arg.__name__
-    elif isinstance(arg, u.quantity.Quantity):
-        return _format_quantity(arg)
-    else:
-        return repr(arg)
-
-
-def _format_kw(keyword):
-    if isinstance(keyword, str):
-        return str(keyword)
-    elif hasattr(keyword, "__name__"):
-        return keyword.__name__
-    else:
-        return repr(keyword)
-
-
-def _exc_str(ex: Exception, color=_exception_color) -> str:
-    """
-    Return a string with an indefinite article and the name of
-    exception `ex`.
-    """
-    if color is None:
-        color = ""
-        return_color = ""
-    else:
-        return_color = _message_color
-    exception_name = ex.__name__
-    use_an = exception_name[0] in "aeiouAEIOU" and exception_name[0:4] != "User"
-    article = "an" if use_an else "a"
-    return f"{article} {color}{exception_name}{return_color}"
-
-
-def _represent_result(result: Any, color=_result_color) -> str:
-    if color is None:
-        color = ""
-        return_color = ""
-    else:
-        return_color = _message_color
-
-    if hasattr(result, "__name__"):
-        return f"{color}{result.__name__}{return_color}"
-    else:
-        return f"{color}{repr(result)}{return_color}"
-
-
-def call_string(
-    f: Callable, args: Any = tuple(), kwargs: Dict = {}, color="", return_color=""
-) -> str:
-    """Return a string with the equivalent call of a function."""
-
-    if color and not return_color:
-        return_color = _message_color
-
-    if not isinstance(args, tuple):
-        args = (args,)
-
-    args_and_kwargs = ""
-
-    for arg in args:
-        args_and_kwargs += f"{_format_arg(arg)}, "
-
-    for kwarg in kwargs:
-        args_and_kwargs += f"{_format_kw(kwarg)}={_format_arg(kwargs[kwarg])}, "
-
-    if args_and_kwargs[-2:] == ", ":
-        args_and_kwargs = args_and_kwargs[:-2]
-
-    return f"{color}{f.__name__}({args_and_kwargs}){return_color}"
diff --git a/requirements/environment.yml b/requirements/environment.yml
index b679836b..879416cb 100644
--- a/requirements/environment.yml
+++ b/requirements/environment.yml
@@ -14,7 +14,6 @@ dependencies:
   - mpmath
   - pandas
   - pillow
-  - colorama
   - pip
   - xarray
   - pip:
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index dc5a0927..7004427c 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,6 +1,5 @@
 # Required Packages w/ version
 astropy >= 4.0
-colorama >= 0.3
 h5py >= 2.8
 lmfit >= 1.0.1
 matplotlib >= 2.0
diff --git a/setup.cfg b/setup.cfg
index 7013131b..6a9abe61 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -17,7 +17,6 @@ classifiers=
   License :: OSI Approved :: BSD License
   Operating System :: OS Independent
   Programming Language :: Python :: 3
-  Programming Language :: Python :: 3.6
   Programming Language :: Python :: 3.7
   Programming Language :: Python :: 3.8
   Programming Language :: Python :: 3.9
@@ -26,13 +25,12 @@ classifiers=
   Topic :: Scientific/Engineering :: Astronomy
 
 [options]
-python_requires = >=3.6
+python_requires = >=3.7
 packages = find:
 include_package_data = True
 setup_requires = setuptools_scm
 install_requires =
   astropy >= 4.0
-  colorama >= 0.3
   numpy >= 1.18.1
   pandas >= 1.0.0
   scipy >= 1.2
diff --git a/tox.ini b/tox.ini
index 6cc25604..cf6bdb75 100644
--- a/tox.ini
+++ b/tox.ini
@@ -44,8 +44,8 @@ setenv =
 commands = sphinx-build -D nbsphinx_execute='never' docs docs/_build/html -b html
 
 # This env requires tox-conda.
-[testenv:py36-conda]
-basepython = python3.6
+[testenv:py37-conda]
+basepython = python3.7
 extras =
 deps =
   lmfit
@@ -55,7 +55,6 @@ conda_deps =
   numpy>=1.18.1
   scipy>=1.2
   astropy>=4.0
-  colorama>=0.3
   pytest>=5.1
   h5py
   matplotlib
@@ -66,8 +65,8 @@ conda_deps =
   sphinx_rtd_theme
 
 # This env tests minimal versions of each dependency.
-[testenv:py36-minimal]
-basepython = python3.6
+[testenv:py37-minimal]
+basepython = python3.7
 extras =
 deps =
   lmfit==1.0.1
@@ -75,7 +74,6 @@ deps =
   numpy==1.18.1
   scipy==1.2
   astropy==4.0
-  colorama==0.3
   h5py==2.8
   matplotlib==2.0
   pytest==5.1
@@ -84,8 +82,8 @@ deps =
 setenv =
     PYTEST_COMMAND = pytest --pyargs plasmapy --durations=25 {toxinidir}/docs --ignore={toxinidir}/docs/conf.py
 
-[testenv:py36-minimal-pypi-import]
-basepython = python3.6
+[testenv:py37-minimal-pypi-import]
+basepython = python3.7
 extras =
 deps =
 commands = python -c 'import plasmapy'


Test patch:
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 465efb06..1af83cce 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -18,15 +18,10 @@ jobs:
       matrix:
         include:
 
-          - name: Python 3.6 with minimal dependencies
+          - name: Python 3.7 with minimal dependencies
             os: ubuntu-latest
-            python: 3.6
-            toxenv: py36-minimal
-
-          - name: Python 3.6
-            os: ubuntu-latest
-            python: 3.6
-            toxenv: py36
+            python: 3.7
+            toxenv: py37-minimal
 
           - name: Python 3.7
             os: ubuntu-latest
diff --git a/plasmapy/particles/tests/test_ionization_collection.py b/plasmapy/particles/tests/test_ionization_collection.py
index d1711adc..d517c1f4 100644
--- a/plasmapy/particles/tests/test_ionization_collection.py
+++ b/plasmapy/particles/tests/test_ionization_collection.py
@@ -157,7 +157,7 @@ def test_simple_equality(self, test_name):
     def test_that_particles_were_set_correctly(self, test_name):
         input_particles = tests[test_name]["inputs"].keys()
         particles = [Particle(input_particle) for input_particle in input_particles]
-        expected_particles = {p.particle for p in particles}
+        expected_particles = {p.symbol for p in particles}
         actual_particles = {
             particle for particle in self.instances[test_name].ionic_fractions.keys()
         }
diff --git a/plasmapy/particles/tests/test_parsing.py b/plasmapy/particles/tests/test_parsing.py
index 627f7a83..abfbc35f 100644
--- a/plasmapy/particles/tests/test_parsing.py
+++ b/plasmapy/particles/tests/test_parsing.py
@@ -13,7 +13,7 @@
     _parse_and_check_atomic_input,
 )
 from plasmapy.particles.special_particles import ParticleZoo
-from plasmapy.utils import call_string
+from plasmapy.utils.code_repr import call_string
 
 
 def _particle_call_string(arg, kwargs=None) -> str:
@@ -95,7 +95,7 @@ def test_alias_dict_properties(alias_dict):
         "He",
         {"Z": 1, "mass_numb": 4},
         {
-            "particle": "He-4 1+",
+            "symbol": "He-4 1+",
             "element": "He",
             "isotope": "He-4",
             "ion": "He-4 1+",
@@ -107,7 +107,7 @@ def test_alias_dict_properties(alias_dict):
         "alpha",
         {},
         {
-            "particle": "He-4 2+",
+            "symbol": "He-4 2+",
             "element": "He",
             "isotope": "He-4",
             "ion": "He-4 2+",
@@ -119,7 +119,7 @@ def test_alias_dict_properties(alias_dict):
         1,
         {},
         {
-            "particle": "H",
+            "symbol": "H",
             "element": "H",
             "isotope": None,
             "ion": None,
@@ -131,7 +131,7 @@ def test_alias_dict_properties(alias_dict):
         "p",
         {},
         {
-            "particle": "p+",
+            "symbol": "p+",
             "element": "H",
             "isotope": "H-1",
             "ion": "p+",
@@ -143,7 +143,7 @@ def test_alias_dict_properties(alias_dict):
         "H",
         {"mass_numb": 2},
         {
-            "particle": "D",
+            "symbol": "D",
             "element": "H",
             "isotope": "D",
             "ion": None,
@@ -155,7 +155,7 @@ def test_alias_dict_properties(alias_dict):
         2,
         {},
         {
-            "particle": "He",
+            "symbol": "He",
             "element": "He",
             "isotope": None,
             "ion": None,
@@ -167,7 +167,7 @@ def test_alias_dict_properties(alias_dict):
         "T",
         {"Z": 0},
         {
-            "particle": "T 0+",
+            "symbol": "T 0+",
             "element": "H",
             "isotope": "T",
             "ion": "T 0+",
@@ -179,7 +179,7 @@ def test_alias_dict_properties(alias_dict):
         "Fe-56+++++++",
         {},
         {
-            "particle": "Fe-56 7+",
+            "symbol": "Fe-56 7+",
             "element": "Fe",
             "isotope": "Fe-56",
             "ion": "Fe-56 7+",
@@ -191,7 +191,7 @@ def test_alias_dict_properties(alias_dict):
         "H-",
         {},
         {
-            "particle": "H 1-",
+            "symbol": "H 1-",
             "element": "H",
             "isotope": None,
             "ion": "H 1-",
@@ -203,7 +203,7 @@ def test_alias_dict_properties(alias_dict):
         "D+",
         {},
         {
-            "particle": "D 1+",
+            "symbol": "D 1+",
             "element": "H",
             "isotope": "D",
             "ion": "D 1+",
@@ -215,7 +215,7 @@ def test_alias_dict_properties(alias_dict):
         "Au",
         {},
         {
-            "particle": "Au",
+            "symbol": "Au",
             "element": "Au",
             "isotope": None,
             "ion": None,
@@ -227,7 +227,7 @@ def test_alias_dict_properties(alias_dict):
         "Ar 2-",
         {},
         {
-            "particle": "Ar 2-",
+            "symbol": "Ar 2-",
             "element": "Ar",
             "isotope": None,
             "ion": "Ar 2-",
@@ -239,7 +239,7 @@ def test_alias_dict_properties(alias_dict):
         "Fe +24",
         {"mass_numb": 56},
         {
-            "particle": "Fe-56 24+",
+            "symbol": "Fe-56 24+",
             "element": "Fe",
             "isotope": "Fe-56",
             "ion": "Fe-56 24+",
@@ -251,7 +251,7 @@ def test_alias_dict_properties(alias_dict):
         "Be-8 +3",
         {},
         {
-            "particle": "Be-8 3+",
+            "symbol": "Be-8 3+",
             "element": "Be",
             "isotope": "Be-8",
             "ion": "Be-8 3+",
@@ -263,7 +263,7 @@ def test_alias_dict_properties(alias_dict):
         "p+",
         {},
         {
-            "particle": "p+",
+            "symbol": "p+",
             "element": "H",
             "isotope": "H-1",
             "ion": "p+",
diff --git a/plasmapy/particles/tests/test_particle_class.py b/plasmapy/particles/tests/test_particle_class.py
index a6049c46..b32f1dcb 100644
--- a/plasmapy/particles/tests/test_particle_class.py
+++ b/plasmapy/particles/tests/test_particle_class.py
@@ -29,7 +29,8 @@
     Particle,
 )
 from plasmapy.particles.special_particles import ParticleZoo
-from plasmapy.utils import call_string, roman
+from plasmapy.utils import roman
+from plasmapy.utils.code_repr import call_string
 from plasmapy.utils.pytest_helpers import run_test_equivalent_calls
 
 # (arg, kwargs, results_dict)
@@ -38,7 +39,7 @@
         "neutron",
         {},
         {
-            "particle": "n",
+            "symbol": "n",
             "element": None,
             "isotope": None,
             "isotope_name": InvalidElementError,
@@ -61,7 +62,7 @@
         "p+",
         {},
         {
-            "particle": "p+",
+            "symbol": "p+",
             "element": "H",
             "element_name": "hydrogen",
             "isotope": "H-1",
@@ -100,7 +101,7 @@
         "p-",
         {},
         {
-            "particle": "p-",
+            "symbol": "p-",
             "element": None,
             "element_name": InvalidElementError,
             "isotope": None,
@@ -125,7 +126,7 @@
         "e-",
         {},
         {
-            "particle": "e-",
+            "symbol": "e-",
             "element": None,
             "element_name": InvalidElementError,
             "isotope": None,
@@ -153,7 +154,7 @@
         "e+",
         {},
         {
-            "particle": "e+",
+            "symbol": "e+",
             "element": None,
             "isotope": None,
             "isotope_name": InvalidElementError,
@@ -185,7 +186,7 @@
         "H",
         {},
         {
-            "particle": "H",
+            "symbol": "H",
             "element": "H",
             "isotope": None,
             "isotope_name": InvalidIsotopeError,
@@ -210,7 +211,7 @@
         "H 1-",
         {},
         {
-            "particle": "H 1-",
+            "symbol": "H 1-",
             "element": "H",
             "isotope": None,
             "isotope_name": InvalidIsotopeError,
@@ -233,7 +234,7 @@
         "H-1 0+",
         {},
         {
-            "particle": "H-1 0+",
+            "symbol": "H-1 0+",
             "element": "H",
             "isotope": "H-1",
             "isotope_name": "hydrogen-1",
@@ -258,7 +259,7 @@
         "D+",
         {},
         {
-            "particle": "D 1+",
+            "symbol": "D 1+",
             "element": "H",
             "element_name": "hydrogen",
             "isotope": "D",
@@ -283,7 +284,7 @@
         "tritium",
         {"Z": 1},
         {
-            "particle": "T 1+",
+            "symbol": "T 1+",
             "element": "H",
             "isotope": "T",
             "isotope_name": "tritium",
@@ -305,7 +306,7 @@
         "Fe",
         {"Z": 17, "mass_numb": 56},
         {
-            "particle": "Fe-56 17+",
+            "symbol": "Fe-56 17+",
             "element": "Fe",
             "element_name": "iron",
             "isotope": "Fe-56",
@@ -329,7 +330,7 @@
         "alpha",
         {},
         {
-            "particle": "He-4 2+",
+            "symbol": "He-4 2+",
             "element": "He",
             "element_name": "helium",
             "isotope": "He-4",
@@ -351,7 +352,7 @@
         "He-4 0+",
         {},
         {
-            "particle": "He-4 0+",
+            "symbol": "He-4 0+",
             "element": "He",
             "isotope": "He-4",
             "mass_energy": 5.971919969131517e-10 * u.J,
@@ -361,7 +362,7 @@
         "Li",
         {"mass_numb": 7},
         {
-            "particle": "Li-7",
+            "symbol": "Li-7",
             "element": "Li",
             "element_name": "lithium",
             "isotope": "Li-7",
@@ -382,7 +383,7 @@
         "Cn-276",
         {"Z": 22},
         {
-            "particle": "Cn-276 22+",
+            "symbol": "Cn-276 22+",
             "element": "Cn",
             "isotope": "Cn-276",
             "isotope_name": "copernicium-276",
@@ -402,7 +403,7 @@
         "muon",
         {},
         {
-            "particle": "mu-",
+            "symbol": "mu-",
             "element": None,
             "isotope": None,
             "isotope_name": InvalidElementError,
@@ -420,7 +421,7 @@
         "nu_tau",
         {},
         {
-            "particle": "nu_tau",
+            "symbol": "nu_tau",
             "element": None,
             "isotope": None,
             "isotope_name": InvalidElementError,
@@ -449,12 +450,12 @@
             'is_category(any_of="boson", exclude="boson")': ParticleError,
         },
     ),
-    (Particle("C"), {}, {"particle": "C", "atomic_number": 6, "element": "C"}),
+    (Particle("C"), {}, {"symbol": "C", "atomic_number": 6, "element": "C"}),
     (
         Particle("C"),
         {"Z": 3, "mass_numb": 14},
         {
-            "particle": "C-14 3+",
+            "symbol": "C-14 3+",
             "element": "C",
             "isotope": "C-14",
             "ionic_symbol": "C-14 3+",
@@ -668,7 +669,7 @@ def test_particle_class_mass_nuclide_mass(isotope: str, ion: str):
         "baryon",
     }:
 
-        particle = Isotope.particle
+        particle = Isotope.symbol
 
         assert Isotope.nuclide_mass == Ion.mass, (
             f"Particle({repr(particle)}).nuclide_mass does not equal "
@@ -1198,8 +1199,8 @@ def test_particles_from_json_string(cls, kwargs, json_string, expected_exception
     if expected_exception is None:
         instance = cls(**kwargs)
         instance_from_json = json_loads_particle(json_string)
-        expected_particle = instance.particle
-        actual_particle = instance_from_json.particle
+        expected_particle = instance.symbol
+        actual_particle = instance_from_json.symbol
         assert expected_particle == actual_particle, pytest.fail(
             f"Expected {expected_particle}\nGot {actual_particle}"
         )
@@ -1222,8 +1223,8 @@ def test_particles_from_json_file(cls, kwargs, json_string, expected_exception):
         test_file_object = io.StringIO(json_string)
         test_file_object.seek(0, io.SEEK_SET)
         instance_from_json = json_load_particle(test_file_object)
-        expected_particle = instance.particle
-        actual_particle = instance_from_json.particle
+        expected_particle = instance.symbol
+        actual_particle = instance_from_json.symbol
         assert expected_particle == actual_particle, pytest.fail(
             f"Expected {expected_particle}\nGot {actual_particle}"
         )
diff --git a/plasmapy/utils/pytest_helpers/pytest_helpers.py b/plasmapy/utils/pytest_helpers/pytest_helpers.py
index d471507b..9843b7d7 100644
--- a/plasmapy/utils/pytest_helpers/pytest_helpers.py
+++ b/plasmapy/utils/pytest_helpers/pytest_helpers.py
@@ -1,4 +1,5 @@
-"""Test helper utilities."""
+"""Utilities to help with testing."""
+
 __all__ = [
     "assert_can_handle_nparray",
     "run_test",
@@ -9,7 +10,6 @@
 import astropy.tests.helper as astrohelper
 import astropy.units as u
 import collections
-import colorama
 import functools
 import inspect
 import numpy as np
@@ -26,26 +26,11 @@
     UnexpectedExceptionFail,
     UnexpectedResultFail,
 )
-from plasmapy.utils.error_messages import _exc_str, _represent_result, call_string
+from plasmapy.utils.code_repr import _name_with_article, _object_name, call_string
 from plasmapy.utils.exceptions import PlasmaPyWarning
 
-# These colors/styles are used to highlight certain parts of the error
-# messages in consistent ways.
-
-_bold = colorama.Style.BRIGHT
-_magenta = colorama.Fore.MAGENTA
-_blue = colorama.Fore.BLUE
-_cyan = colorama.Fore.CYAN
-_red = colorama.Fore.RED
-
-_exception_color = f"{_magenta}{_bold}"
-_type_color = f"{_magenta}{_bold}"
-_func_color = f"{_cyan}{_bold}"
-_result_color = f"{_blue}{_bold}"
-_message_color = f"{_red}{_bold}"
 
-
-def _process_input(wrapped_function: Callable):
+def _process_input(wrapped_function: Callable):  # coverage: ignore
     """
     Allow `run_test` to take a single positional argument that is a
     `list` or `tuple` in lieu of using multiple positional/keyword
@@ -86,11 +71,11 @@ def wrapper(*args, **kwargs):
 def run_test(
     func,
     args: Any = (),
-    kwargs: Dict = {},
+    kwargs: Dict = None,
     expected_outcome: Any = None,
     rtol: float = 0.0,
     atol: float = 0.0,
-):
+):  # coverage: ignore
     """
     Test that a function or class returns the expected result, raises
     the expected exception, or issues an expected warning for the
@@ -230,6 +215,9 @@ def test_func(inputs):
 
     """
 
+    if kwargs is None:
+        kwargs = {}
+
     if not isinstance(args, tuple):
         args = (args,)
 
@@ -242,9 +230,7 @@ def test_func(inputs):
     # messages, we can make it easier to reproduce the error in an
     # interactive session.
 
-    call_str = call_string(
-        func, args, kwargs, color=_func_color, return_color=_message_color
-    )
+    call_str = call_string(func, args, kwargs)
 
     # There are many possibilities for expected outcomes that we must
     # keep track of, including exceptions being raised and warnings
@@ -297,22 +283,22 @@ def test_func(inputs):
             else:
                 raise UnexpectedExceptionFail(
                     f"The command {call_str} did not specifically raise "
-                    f"{_exc_str(expected_exception)} as expected, but "
-                    f"instead raised {_exc_str(resulting_exception)} "
+                    f"{_name_with_article(expected_exception)} as expected, but "
+                    f"instead raised {_name_with_article(resulting_exception)} "
                     f"which is a subclass of the expected exception."
                 )
         except Exception as exc_unexpected_exception:
             unexpected_exception = exc_unexpected_exception.__reduce__()[0]
             raise UnexpectedExceptionFail(
                 f"The command {call_str} did not raise "
-                f"{_exc_str(expected_exception)} as expected, "
-                f"but instead raised {_exc_str(unexpected_exception)}."
+                f"{_name_with_article(expected_exception)} as expected, "
+                f"but instead raised {_name_with_article(unexpected_exception)}."
             ) from exc_unexpected_exception
         else:
             raise MissingExceptionFail(
                 f"The command {call_str} did not raise "
-                f"{_exc_str(expected_exception)} as expected, but instead "
-                f"returned {_represent_result(result)}."
+                f"{_name_with_article(expected_exception)} as expected, but instead "
+                f"returned {_object_name(result)}."
             )
 
     try:
@@ -321,15 +307,15 @@ def test_func(inputs):
     except pytest.raises.Exception as missing_warning:
         raise MissingWarningFail(
             f"The command {call_str} should issue "
-            f"{_exc_str(expected['warning'])}, but instead returned "
-            f"{_represent_result(result)}."
+            f"{_name_with_article(expected['warning'])}, but instead returned "
+            f"{_object_name(result)}."
         ) from missing_warning
     except Exception as exception_no_warning:
         raise UnexpectedExceptionFail(
             f"The command {call_str} unexpectedly raised "
-            f"{_exc_str(exception_no_warning.__reduce__()[0])} "
+            f"{_name_with_article(exception_no_warning.__reduce__()[0])} "
             f"instead of returning the expected value of "
-            f"{_represent_result(expected['result'])}."
+            f"{_object_name(expected['result'])}."
         ) from exception_no_warning
 
     if isinstance(expected["result"], u.UnitBase):
@@ -338,25 +324,25 @@ def test_func(inputs):
             if result != expected["result"]:
                 raise u.UnitsError(
                     f"The command {call_str} returned "
-                    f"{_represent_result(result)} instead of the expected "
-                    f"value of {_represent_result(expected['result'])}."
+                    f"{_object_name(result)} instead of the expected "
+                    f"value of {_object_name(expected['result'])}."
                 )
             return None
 
         if not isinstance(result, (u.Quantity, const.Constant, const.EMConstant)):
             raise u.UnitsError(
                 f"The command {call_str} returned "
-                f"{_represent_result(result)} instead of a quantity or "
+                f"{_object_name(result)} instead of a quantity or "
                 f"constant with units of "
-                f"{_represent_result(expected['result'])}."
+                f"{_object_name(expected['result'])}."
             )
 
         if result.unit != expected["result"]:
             raise u.UnitsError(
                 f"The command {call_str} returned "
-                f"{_represent_result(result)}, which has units of "
+                f"{_object_name(result)}, which has units of "
                 f"{result.unit} instead of the expected units of "
-                f"{_represent_result(expected['result'])}."
+                f"{_object_name(expected['result'])}."
             )
 
         return None
@@ -365,9 +351,9 @@ def test_func(inputs):
         if not result.unit == expected["result"].unit:
             raise u.UnitsError(
                 f"The command {call_str} returned "
-                f"{_represent_result(result)} which has different units "
+                f"{_object_name(result)} which has different units "
                 f"than the expected result of "
-                f"{_represent_result(expected['result'])}."
+                f"{_object_name(expected['result'])}."
             )
 
         if np.allclose(result.value, expected["result"].value):
@@ -379,11 +365,11 @@ def test_func(inputs):
     if type(result) != type(expected["result"]):
         raise TypeMismatchFail(
             f"The command {call_str} returned "
-            f"{_represent_result(result)} which has type "
-            f"{_represent_result(type(result), color=_type_color)}, "
+            f"{_object_name(result)} which has type "
+            f"{_object_name(type(result))}, "
             f"instead of the expected value of "
-            f"{_represent_result(expected['result'])} which has type "
-            f"{_represent_result(type(expected['result']), color=_type_color)}."
+            f"{_object_name(expected['result'])} which has type "
+            f"{_object_name(type(expected['result']))}."
         )
 
     try:
@@ -391,8 +377,8 @@ def test_func(inputs):
             return None
     except Exception as exc_equality:  # coverage: ignore
         raise TypeError(
-            f"The equality of {_represent_result(result)} and "
-            f"{_represent_result(expected['result'])} "
+            f"The equality of {_object_name(result)} and "
+            f"{_object_name(expected['result'])} "
             f"cannot be evaluated."
         ) from exc_equality
 
@@ -410,8 +396,8 @@ def test_func(inputs):
 
     errmsg = (
         f"The command {call_str} returned "
-        f"{_represent_result(result)} instead of the expected "
-        f"value of {_represent_result(expected['result'])}."
+        f"{_object_name(result)} instead of the expected "
+        f"value of {_object_name(expected['result'])}."
     )
 
     if atol or rtol:
@@ -637,7 +623,7 @@ def run_test_equivalent_calls(*test_inputs, require_same_type: bool = True):
 
 
 def assert_can_handle_nparray(
-    function_to_test, insert_some_nans=[], insert_all_nans=[], kwargs={}
+    function_to_test, insert_some_nans=None, insert_all_nans=None, kwargs=None,
 ):
     """
     Test for ability to handle numpy array quantities.
@@ -680,6 +666,15 @@ def assert_can_handle_nparray(
     >>> assert_can_handle_nparray(gyrofrequency, kwargs={"signed": False})
     """
 
+    if insert_some_nans is None:
+        insert_some_nans = []
+
+    if insert_all_nans is None:
+        insert_all_nans = []
+
+    if kwargs is None:
+        kwargs = {}
+
     def _prepare_input(
         param_name, param_default, insert_some_nans, insert_all_nans, kwargs
     ):
diff --git a/plasmapy/utils/pytest_helpers/tests/test_pytest_helpers.py b/plasmapy/utils/pytest_helpers/tests/test_pytest_helpers.py
index 716512df..dc278c3f 100644
--- a/plasmapy/utils/pytest_helpers/tests/test_pytest_helpers.py
+++ b/plasmapy/utils/pytest_helpers/tests/test_pytest_helpers.py
@@ -12,7 +12,7 @@
     UnexpectedExceptionFail,
     UnexpectedResultFail,
 )
-from plasmapy.utils import call_string
+from plasmapy.utils.code_repr import call_string
 from plasmapy.utils.exceptions import PlasmaPyError, PlasmaPyWarning
 from plasmapy.utils.pytest_helpers import run_test, run_test_equivalent_calls
 
@@ -50,31 +50,6 @@ def return_arg(arg: Any, should_warn: bool = False) -> Any:
     return arg
 
 
-# function, args, kwargs, expected
-call_string_table = [
-    (generic_function, (), {}, "generic_function()"),
-    (generic_function, (1), {}, "generic_function(1)"),
-    (generic_function, ("x"), {}, "generic_function('x')"),
-    (generic_function, (1, "b", {}), {}, "generic_function(1, 'b', {})"),
-    (generic_function, (), {"kw": 1}, "generic_function(kw=1)"),
-    (generic_function, (), {"x": "c"}, "generic_function(x='c')"),
-    (
-        generic_function,
-        (1, "b"),
-        {"b": 42, "R2": "D2"},
-        "generic_function(1, 'b', b=42, R2='D2')",
-    ),
-    (run_test, run_test, {run_test: run_test}, "run_test(run_test, run_test=run_test)"),
-]
-
-
-@pytest.mark.parametrize("function,args,kwargs,expected", call_string_table)
-def test_call_string(function, args, kwargs, expected):
-    """Tests that call_string returns a string that is
-    equivalent to the function call."""
-    assert expected == call_string(function, args, kwargs)
-
-
 f_args_kwargs_expected_whaterror = [
     [adams_number, 0, {"y": 1}, 42, None],
     [adams_number, (1,), {"y": 1}, 42, None],
diff --git a/plasmapy/utils/tests/test_code_repr.py b/plasmapy/utils/tests/test_code_repr.py
new file mode 100644
index 00000000..5db88b2b
--- /dev/null
+++ b/plasmapy/utils/tests/test_code_repr.py
@@ -0,0 +1,471 @@
+"""Tests for code representation functions."""
+
+import numpy as np
+import pytest
+
+from astropy import units as u
+from collections import namedtuple
+
+from plasmapy.utils.code_repr import (
+    _code_repr_of_arg,
+    _code_repr_of_args_and_kwargs,
+    _code_repr_of_ndarray,
+    _code_repr_of_quantity,
+    _name_with_article,
+    _object_name,
+    _string_together_warnings_for_printing,
+    attribute_call_string,
+    call_string,
+    method_call_string,
+)
+
+
+def generic_function(*args, **kwargs):
+    return None
+
+
+def adams_number():
+    return 42
+
+
+function_case = namedtuple("function_case", ("func", "args", "kwargs", "expected"))
+
+
+@pytest.mark.parametrize(
+    "func, args, kwargs, expected",
+    [
+        function_case(
+            func=generic_function, args=(), kwargs={}, expected="generic_function()"
+        ),
+        function_case(
+            func=generic_function, args=(1), kwargs={}, expected="generic_function(1)"
+        ),
+        function_case(
+            func=generic_function,
+            args=("x"),
+            kwargs={},
+            expected="generic_function('x')",
+        ),
+        function_case(
+            func=generic_function,
+            args=(1, "b", {}),
+            kwargs={},
+            expected="generic_function(1, 'b', {})",
+        ),
+        function_case(
+            func=generic_function,
+            args=(),
+            kwargs={"kw": 1},
+            expected="generic_function(kw=1)",
+        ),
+        function_case(
+            func=generic_function,
+            args=(),
+            kwargs={"x": "c"},
+            expected="generic_function(x='c')",
+        ),
+        function_case(
+            func=generic_function,
+            args=(1, 2, 3),
+            kwargs={"a": 4, "b": 5, "c": 6},
+            expected="generic_function(1, 2, 3, a=4, b=5, c=6)",
+        ),
+        function_case(
+            func=generic_function,
+            args=np.array((1.0, 2.0)),
+            kwargs={},
+            expected="generic_function(np.array([1., 2.]))",
+        ),
+        function_case(
+            func=generic_function,
+            args=np.array((3.0, 4.0)) * u.m,
+            kwargs={},
+            expected="generic_function(np.array([3., 4.])*u.m)",
+        ),
+        function_case(
+            func=generic_function,
+            args=(ValueError,),
+            kwargs={"x": TypeError},
+            expected="generic_function(ValueError, x=TypeError)",
+        ),
+        function_case(
+            func=generic_function,
+            args=(),
+            kwargs={"bowl": super},
+            expected="generic_function(bowl=super)",
+        ),
+        function_case(
+            func=generic_function,
+            args=(1, "b"),
+            kwargs={"b": 42, "R2": "D2"},
+            expected="generic_function(1, 'b', b=42, R2='D2')",
+        ),
+        function_case(
+            func=generic_function,
+            args=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
+            kwargs={},
+            expected="generic_function(np.array([1, 2, 3, 4, 5, 6, 7, 8, ...]))",
+        ),
+        function_case(
+            func=generic_function,
+            args=[(1, "a")],
+            kwargs={},
+            expected="generic_function((1, 'a'))",
+        ),
+        function_case(
+            func=generic_function,
+            args=([1, "a"],),
+            kwargs={},
+            expected="generic_function([1, 'a'])",
+        ),
+    ],
+)
+def test_call_string(func, args, kwargs, expected):
+    """
+    Tests that call_string returns a string that is equivalent to the
+    function call.
+    """
+    actual = call_string(func, args, kwargs, max_items=8)
+    assert actual == expected, (
+        "When call_string is called with:\n"
+        f"  function: {func.__name__}\n"
+        f"  args:     {args}\n"
+        f"  kwargs:   {kwargs}\n"
+        "the actual result does not match the expected result:\n"
+        f"  expected: {repr(expected)}\n"
+        f"  actual:   {repr(actual)}"
+    )
+
+
+class SampleClass:
+    def method(self, *args, **kwargs):
+        pass
+
+    @property
+    def attr(self):
+        pass
+
+
+method_case = namedtuple(
+    "method_case",
+    ("args_to_cls", "kwargs_to_cls", "args_to_method", "kwargs_to_method", "expected",),
+)
+
+
+@pytest.mark.parametrize(
+    "args_to_cls, kwargs_to_cls, args_to_method, kwargs_to_method, expected",
+    [
+        method_case(
+            args_to_cls=(),
+            kwargs_to_cls={},
+            args_to_method=(),
+            kwargs_to_method={},
+            expected="SampleClass().method()",
+        ),
+        method_case(
+            args_to_cls=(1, 2),
+            kwargs_to_cls={"a": "x", "b": "y"},
+            args_to_method=(3, 4.2),
+            kwargs_to_method={"q": UserWarning},
+            expected="SampleClass(1, 2, a='x', b='y').method(3, 4.2, q=UserWarning)",
+        ),
+        method_case(
+            args_to_cls=(Exception),
+            kwargs_to_cls={},
+            args_to_method=(SampleClass, adams_number),
+            kwargs_to_method={"r": 5.3 * u.m},
+            expected="SampleClass(Exception).method(SampleClass, adams_number, r=5.3*u.m)",
+        ),
+        method_case(
+            args_to_cls=1,
+            kwargs_to_cls={},
+            args_to_method=2,
+            kwargs_to_method={},
+            expected="SampleClass(1).method(2)",
+        ),
+    ],
+)
+def test_method_call_string(
+    args_to_cls, kwargs_to_cls, args_to_method, kwargs_to_method, expected
+):
+    """Test that `method_call_string` returns the expected results."""
+    actual = method_call_string(
+        cls=SampleClass,
+        method="method",
+        args_to_cls=args_to_cls,
+        kwargs_to_cls=kwargs_to_cls,
+        args_to_method=args_to_method,
+        kwargs_to_method=kwargs_to_method,
+    )
+    assert actual == expected, (
+        "When method_call_string is called with:\n"
+        f"  cls:      SampleClass\n"
+        f"  method:   'method'\n"
+        f"  c_args:   {args_to_cls}\n"
+        f"  c_kwargs: {kwargs_to_cls}\n"
+        f"  m_args:   {args_to_method}\n"
+        f"  m_kwargs: {kwargs_to_method}\n"
+        "the actual outcome does not match the expected result:\n"
+        f"  expected: {repr(expected)}\n"
+        f"  actual:   {repr(actual)}"
+    )
+
+
+attribute_case = namedtuple(
+    "attribute_case", ("args_to_cls", "kwargs_to_cls", "expected",),
+)
+
+
+@pytest.mark.parametrize(
+    "args_to_cls, kwargs_to_cls, expected",
+    [
+        attribute_case(args_to_cls=(), kwargs_to_cls={}, expected="SampleClass().attr"),
+        attribute_case(
+            args_to_cls=(1, 2),
+            kwargs_to_cls={"a": "x", "b": "y"},
+            expected="SampleClass(1, 2, a='x', b='y').attr",
+        ),
+        attribute_case(args_to_cls=1, kwargs_to_cls={}, expected="SampleClass(1).attr"),
+        attribute_case(
+            args_to_cls={"key": "value"},
+            kwargs_to_cls={},
+            expected="SampleClass({'key': 'value'}).attr",
+        ),
+    ],
+)
+def test_attribute_call_string(args_to_cls, kwargs_to_cls, expected):
+    """Test that `attribute_call_string` returns the expected results."""
+    actual = attribute_call_string(SampleClass, "attr", args_to_cls, kwargs_to_cls)
+    assert actual == expected, (
+        "When attribute_call_string is called with:\n"
+        f"  cls:      SampleClass\n"
+        f"  attr:     'attr'\n"
+        f"  c_args:   {args_to_cls}\n"
+        f"  c_kwargs: {kwargs_to_cls}\n"
+        "the actual outcome does not match the expected result:\n"
+        f"  expected: {repr(expected)}\n"
+        f"  actual:   {repr(actual)}"
+    )
+
+
+ndarray_case = namedtuple("ndarray_case", ("array_inputs", "max_items", "expected"))
+
+
+@pytest.mark.parametrize(
+    "array_inputs, max_items, expected",
+    [
+        ndarray_case(array_inputs=[0], max_items=np.inf, expected="np.array([0])"),
+        ndarray_case(
+            array_inputs=[0.0, 1.0], max_items=np.inf, expected="np.array([0., 1.])"
+        ),
+        ndarray_case(
+            array_inputs=[0.0, 1.142],
+            max_items=np.inf,
+            expected="np.array([0., 1.142])",
+        ),
+        ndarray_case(
+            array_inputs=[[0, 1, 2], [3, 4, 5]],
+            max_items=np.inf,
+            expected="np.array([[0, 1, 2], [3, 4, 5]])",
+        ),
+        ndarray_case(
+            array_inputs=[np.inf], max_items=np.inf, expected="np.array([np.inf])"
+        ),
+        ndarray_case(
+            array_inputs=[np.nan], max_items=np.inf, expected="np.array([np.nan])"
+        ),
+        ndarray_case(
+            array_inputs=[np.nan, np.inf, -np.inf],
+            max_items=np.inf,
+            expected="np.array([np.nan, np.inf, -np.inf])",
+        ),
+        ndarray_case(array_inputs=[1], max_items=1, expected="np.array([1])"),
+        ndarray_case(array_inputs=[1, 2], max_items=1, expected="np.array([1, ...])"),
+        ndarray_case(
+            array_inputs=[1, 2, 3, 4], max_items=2, expected="np.array([1, 2, ...])"
+        ),
+        ndarray_case(
+            array_inputs=[[1, 2, 3], [4, 5, 6]],
+            max_items=5,
+            expected="np.array([[1, 2, 3], [4, 5, ...]])",
+        ),
+        ndarray_case(
+            array_inputs=[[[1, 2], [3, 4]], [[5, 6], [7, 8]]],
+            max_items=1,
+            expected="np.array([[[1, ...]]])",
+        ),
+    ],
+)
+def test__code_repr_of_ndarray(array_inputs, max_items, expected):
+    """
+    Test that `numpy.ndarray` objects get converted into a string as
+    expected.  Subsequently, test that evaluating the code representation
+    of an ndarray returns an array equal to the array created from
+    `array_inputs`.
+    """
+    array = np.array(array_inputs)
+    actual = _code_repr_of_ndarray(array, max_items=max_items)
+    assert actual == expected, (
+        f"The representation of an ndarray for array_inputs = {array_inputs} "
+        f"and max_items = {max_items} is not the expected result:\n"
+        f"  expected: {repr(expected)}\n"
+        f"  actual:   {repr(actual)}\n"
+    )
+
+    if max_items >= array.size:
+        recreated_array = eval(actual)
+        assert np.allclose(array, recreated_array, atol=1e-15, equal_nan=True), (
+            f"Evaluating the representation of an ndarray for array_inputs = "
+            f"{array_inputs} and max_items = {max_items} does not recreate "
+            f"the original array, as expected."
+            f"array:           {array}\n"
+            f"recreated_array: {recreated_array}"
+        )
+
+
+quantity_case = namedtuple("QuantityTestCases", ("quantity", "expected"))
+
+
+@pytest.mark.parametrize(
+    "quantity, expected",
+    [
+        quantity_case(quantity=5.3 * u.m, expected="5.3*u.m"),
+        quantity_case(quantity=5.4 / u.m, expected="5.4/u.m"),
+        quantity_case(quantity=5.5 * u.m ** -2, expected="5.5*u.m**-2"),
+        quantity_case(
+            quantity=u.Quantity(5.0), expected="5.0*u.dimensionless_unscaled"
+        ),
+        quantity_case(
+            quantity=np.array([3.5, 4.2]) * u.m ** -2.5,
+            expected="np.array([3.5, 4.2])*u.m**-2.5",
+        ),
+    ],
+)
+def test__code_repr_of_quantity(quantity, expected):
+    """
+    Test that `astropy.units.Quantity` objects get converted into a
+    string as expected.
+    """
+    actual = _code_repr_of_quantity(quantity)
+    assert actual == expected, (
+        f"_code_repr_of_quantity for {quantity} is not producing the "
+        f"expected result:\n"
+        f"expected: {repr(expected)}\n"
+        f"actual:   {repr(actual)}"
+    )
+
+
+def test__string_together_warnings_for_printing():
+    """Test that warning names and messages get strung together correctly."""
+    warnings = [UserWarning, DeprecationWarning]
+    warning_messages = ["msg1", "msg2"]
+    expected = "UserWarning: msg1\n\nDeprecationWarning: msg2"
+    actual = _string_together_warnings_for_printing(warnings, warning_messages)
+    assert actual == expected, (
+        f"_string_together_warnings_for_printing is not producing the "
+        f"expected result:\n"
+        f"  expected: {repr(expected)}\n"
+        f"  actual: {repr(actual)}"
+    )
+
+
+@pytest.mark.parametrize(
+    "arg, expected",
+    [
+        (1, "1"),
+        ("asdf", "'asdf'"),
+        (3.42, "3.42"),
+        ([3.42, 3.84], "[3.42, 3.84]"),
+        (4.2 * u.m, "4.2*u.m"),
+        (np.array([1, 2, 3]), "np.array([1, 2, 3])"),
+        (np.array([[1, 2], [3, 4]]), "np.array([[1, 2], [3, 4]])"),
+        (np.array([1.0, 2.0, 3.0]) * u.m / u.s, "np.array([1., 2., 3.])*u.m/u.s"),
+    ],
+)
+def test__code_repr_of_arg(arg, expected):
+    """
+    Test that _code_repr_of_arg correctly transforms arguments into a
+    `str` the represents how the arg would appear in code.
+    """
+    code_repr_of_arg = _code_repr_of_arg(arg)
+    assert code_repr_of_arg == expected, (
+        f"_code_repr_of_arg is not returning the expected result.\n"
+        f"arg = {arg}\n"
+        f"expected:         {expected}\n"
+        f"code_repr_of_arg: {code_repr_of_arg}"
+    )
+
+
+@pytest.mark.parametrize(
+    "args, kwargs, expected",
+    [
+        ((), {}, ""),
+        (1, {"a": "b"}, "1, a='b'"),
+        ((1, 2, 3), {"a": "A", "b": "B"}, "1, 2, 3, a='A', b='B'"),
+        (np.array([1.0, 2.0]), {"k": 3}, "np.array([1., 2.]), k=3"),
+        (np.array([1.0, 2.0]) * u.m / u.s, {}, "np.array([1., 2.])*u.m/u.s"),
+    ],
+)
+def test__code_repr_of_args_and_kwargs(args, kwargs, expected):
+    """
+    Test that `_code_repr_of_args_and_kwargs` returns a string containing
+    the positional and keyword arguments, as they would appear in a
+    function call.
+    """
+    args_and_kwargs = _code_repr_of_args_and_kwargs(args, kwargs)
+    assert args_and_kwargs == expected, (
+        f"_code_repr_of_args_and_kwargs with the following arguments:\n"
+        f"  args:   {args}\n"
+        f"  kwargs: {kwargs}\n"
+        f"is not returning the expected string:\n"
+        f"  expected: {repr(expected)}\n"
+    )
+
+
+@pytest.mark.parametrize(
+    "obj, expected",
+    [
+        (ArithmeticError, "an ArithmeticError"),
+        (EOFError, "an EOFError"),
+        (IndexError, "an IndexError"),
+        (OSError, "an OSError"),
+        (TypeError, "a TypeError"),
+        (UserWarning, "a UserWarning"),
+        (UnicodeError, "a UnicodeError"),
+        (ValueError, "a ValueError"),
+    ],
+)
+def test__name_with_article(obj, expected):
+    """
+    Test that `_name_with_article` returns the expected string, which
+    contains ``"a "`` or ``"an "`` followed by the name of ``obj``.
+    """
+    name_with_article = _name_with_article(obj)
+    assert name_with_article == expected, (
+        f"When calling _name_with_article for {obj}, expecting "
+        f"{repr(expected)} but got {repr(name_with_article)}."
+    )
+
+
+@pytest.mark.parametrize(
+    "obj, showmodule, expected_name",
+    [
+        (ValueError, False, "ValueError"),
+        (TypeError, True, "TypeError"),
+        (np.ndarray, False, "ndarray"),
+        (np.ndarray, True, "np.ndarray"),
+        (super, False, "super"),
+        (super, True, "super"),  # hide module for builtins even if showmodule==True
+        (u.Unit, False, "Unit"),
+        (u.Unit, True, "u.Unit"),
+    ],
+)
+def test__object_name(obj, showmodule, expected_name):
+    """Test that `_object_name` produces the expected output."""
+    actual_name = _object_name(obj, showmodule=showmodule)
+    assert actual_name == expected_name, (
+        f"For obj = {repr(obj)}, the expected output of _obj_name with "
+        f"showmodule = {showmodule} does not match the actual output:\n"
+        f"  expected: {expected_name}\n"
+        f"  actual:   {actual_name}"
+    )