diff --git a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
index 4d48437e0988..2bcd5d91d7a6 100644
--- a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
+++ b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
@@ -10,6 +10,7 @@ The following modules are deprecated:
 
 The following classes, methods, functions, and attributes are deprecated:
 
+- ``afm.parse_afm``,
 - ``Annotation.arrow``,
 - ``cbook.GetRealpathAndStat``, ``cbook.Locked``,
 - ``cbook.is_numlike`` (use ``isinstance(..., numbers.Number)`` instead),
diff --git a/lib/matplotlib/afm.py b/lib/matplotlib/afm.py
index cfdcd55273e3..df8fe8010867 100644
--- a/lib/matplotlib/afm.py
+++ b/lib/matplotlib/afm.py
@@ -32,14 +32,18 @@
     >>> afm.get_bbox_char('!')
     [130, -9, 238, 676]
 
+As in the Adobe Font Metrics File Format Specification, all dimensions
+are given in units of 1/1000 of the scale factor (point size) of the font
+being used.
 """
 
+from collections import namedtuple
 import re
 import sys
 
 from ._mathtext_data import uni2type1
+from matplotlib.cbook import deprecated
 
-# Convert string the a python type
 
 # some afm files have floats where we are expecting ints -- there is
 # probably a better way to handle this (support floats, round rather
@@ -47,7 +51,6 @@
 # this change to _to_int should at least prevent mpl from crashing on
 # these JDH (2009-11-06)
 
-
 def _to_int(x):
     return int(float(x))
 
@@ -169,16 +172,42 @@ def _parse_header(fh):
     raise RuntimeError('Bad parse')
 
 
+CharMetrics = namedtuple('CharMetrics', 'width, name, bbox')
+CharMetrics.__doc__ = """
+    Represents the character metrics of a single character.
+
+    Notes
+    -----
+    The fields do currently only describe a subset of character metrics
+    information defined in the AFM standard.
+    """
+CharMetrics.width.__doc__ = """The character width (WX)."""
+CharMetrics.name.__doc__ = """The character name (N)."""
+CharMetrics.bbox.__doc__ = """
+    The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
+
+
 def _parse_char_metrics(fh):
     """
-    Return a character metric dictionary.  Keys are the ASCII num of
-    the character, values are a (*wx*, *name*, *bbox*) tuple, where
-    *wx* is the character width, *name* is the postscript language
-    name, and *bbox* is a (*llx*, *lly*, *urx*, *ury*) tuple.
+    Parse the given filehandle for character metrics information and return
+    the information as dicts.
 
+    It is assumed that the file cursor is on the line behind
+    'StartCharMetrics'.
+
+    Returns
+    -------
+    ascii_d : dict
+         A mapping "ASCII num of the character" to `.CharMetrics`.
+    name_d : dict
+         A mapping "character name" to `.CharMetrics`.
+
+    Notes
+    -----
     This function is incomplete per the standard, but thus far parses
     all the sample afm files tried.
     """
+    required_keys = {'C', 'WX', 'N', 'B'}
 
     ascii_d = {}
     name_d = {}
@@ -191,21 +220,22 @@ def _parse_char_metrics(fh):
         # Split the metric line into a dictionary, keyed by metric identifiers
         vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s)
         # There may be other metrics present, but only these are needed
-        if not {'C', 'WX', 'N', 'B'}.issubset(vals):
+        if not required_keys.issubset(vals):
             raise RuntimeError('Bad char metrics line: %s' % line)
         num = _to_int(vals['C'])
         wx = _to_float(vals['WX'])
         name = vals['N']
         bbox = _to_list_of_floats(vals['B'])
         bbox = list(map(int, bbox))
+        metrics = CharMetrics(wx, name, bbox)
         # Workaround: If the character name is 'Euro', give it the
         # corresponding character code, according to WinAnsiEncoding (see PDF
         # Reference).
         if name == 'Euro':
             num = 128
         if num != -1:
-            ascii_d[num] = (wx, name, bbox)
-        name_d[name] = (wx, bbox)
+            ascii_d[num] = metrics
+        name_d[name] = metrics
     raise RuntimeError('Bad parse')
 
 
@@ -241,55 +271,80 @@ def _parse_kern_pairs(fh):
     raise RuntimeError('Bad kern pairs parse')
 
 
+CompositePart = namedtuple('CompositePart', 'name, dx, dy')
+CompositePart.__doc__ = """
+    Represents the information on a composite element of a composite char."""
+CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'."""
+CompositePart.dx.__doc__ = """x-displacement of the part from the origin."""
+CompositePart.dy.__doc__ = """y-displacement of the part from the origin."""
+
+
 def _parse_composites(fh):
     """
-    Return a composites dictionary.  Keys are the names of the
-    composites.  Values are a num parts list of composite information,
-    with each element being a (*name*, *dx*, *dy*) tuple.  Thus a
-    composites line reading:
+    Parse the given filehandle for composites information return them as a
+    dict.
+
+    It is assumed that the file cursor is on the line behind 'StartComposites'.
+
+    Returns
+    -------
+    composites : dict
+        A dict mapping composite character names to a parts list. The parts
+        list is a list of `.CompositePart` entries describing the parts of
+        the composite.
+
+    Example
+    -------
+    A composite definition line::
 
       CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ;
 
     will be represented as::
 
-      d['Aacute'] = [ ('A', 0, 0), ('acute', 160, 170) ]
+      composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0),
+                              CompositePart(name='acute', dx=160, dy=170)]
 
     """
-    d = {}
+    composites = {}
     for line in fh:
         line = line.rstrip()
         if not line:
             continue
         if line.startswith(b'EndComposites'):
-            return d
+            return composites
         vals = line.split(b';')
         cc = vals[0].split()
         name, numParts = cc[1], _to_int(cc[2])
         pccParts = []
         for s in vals[1:-1]:
             pcc = s.split()
-            name, dx, dy = pcc[1], _to_float(pcc[2]), _to_float(pcc[3])
-            pccParts.append((name, dx, dy))
-        d[name] = pccParts
+            part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3]))
+            pccParts.append(part)
+        composites[name] = pccParts
 
     raise RuntimeError('Bad composites parse')
 
 
 def _parse_optional(fh):
     """
-    Parse the optional fields for kern pair data and composites
-
-    return value is a (*kernDict*, *compositeDict*) which are the
-    return values from :func:`_parse_kern_pairs`, and
-    :func:`_parse_composites` if the data exists, or empty dicts
-    otherwise
+    Parse the optional fields for kern pair data and composites.
+
+    Returns
+    -------
+    kern_data : dict
+        A dict containing kerning information. May be empty.
+        See `._parse_kern_pairs`.
+    composites : dict
+        A dict containing composite information. May be empty.
+        See `._parse_composites`.
     """
     optional = {
         b'StartKernData': _parse_kern_pairs,
         b'StartComposites':  _parse_composites,
         }
 
-    d = {b'StartKernData': {}, b'StartComposites': {}}
+    d = {b'StartKernData': {},
+         b'StartComposites': {}}
     for line in fh:
         line = line.rstrip()
         if not line:
@@ -299,47 +354,53 @@ def _parse_optional(fh):
         if key in optional:
             d[key] = optional[key](fh)
 
-    l = (d[b'StartKernData'], d[b'StartComposites'])
-    return l
+    return d[b'StartKernData'], d[b'StartComposites']
 
 
+@deprecated("3.0", "Use the class AFM instead.")
 def parse_afm(fh):
+    return _parse_afm(fh)
+
+
+def _parse_afm(fh):
     """
-    Parse the Adobe Font Metics file in file handle *fh*. Return value
-    is a (*dhead*, *dcmetrics_ascii*, *dmetrics_name*, *dkernpairs*,
-    *dcomposite*) tuple where
-    *dhead* is a :func:`_parse_header` dict,
-    *dcmetrics_ascii* and *dcmetrics_name* are the two resulting dicts
-    from :func:`_parse_char_metrics`,
-    *dkernpairs* is a :func:`_parse_kern_pairs` dict (possibly {}) and
-    *dcomposite* is a :func:`_parse_composites` dict (possibly {})
+    Parse the Adobe Font Metrics file in file handle *fh*.
+
+    Returns
+    -------
+    header : dict
+        A header dict. See :func:`_parse_header`.
+    cmetrics_by_ascii : dict
+        From :func:`_parse_char_metrics`.
+    cmetrics_by_name : dict
+        From :func:`_parse_char_metrics`.
+    kernpairs : dict
+        From :func:`_parse_kern_pairs`.
+    composites : dict
+        From :func:`_parse_composites`
+
     """
     _sanity_check(fh)
-    dhead = _parse_header(fh)
-    dcmetrics_ascii, dcmetrics_name = _parse_char_metrics(fh)
-    doptional = _parse_optional(fh)
-    return dhead, dcmetrics_ascii, dcmetrics_name, doptional[0], doptional[1]
+    header = _parse_header(fh)
+    cmetrics_by_ascii, cmetrics_by_name = _parse_char_metrics(fh)
+    kernpairs, composites = _parse_optional(fh)
+    return header, cmetrics_by_ascii, cmetrics_by_name, kernpairs, composites
 
 
 class AFM(object):
 
     def __init__(self, fh):
-        """
-        Parse the AFM file in file object *fh*
-        """
-        (dhead, dcmetrics_ascii, dcmetrics_name, dkernpairs, dcomposite) = \
-            parse_afm(fh)
-        self._header = dhead
-        self._kern = dkernpairs
-        self._metrics = dcmetrics_ascii
-        self._metrics_by_name = dcmetrics_name
-        self._composite = dcomposite
+        """Parse the AFM file in file object *fh*."""
+        (self._header,
+         self._metrics,
+         self._metrics_by_name,
+         self._kern,
+         self._composite) = _parse_afm(fh)
 
     def get_bbox_char(self, c, isord=False):
         if not isord:
             c = ord(c)
-        wx, name, bbox = self._metrics[c]
-        return bbox
+        return self._metrics[c].bbox
 
     def string_width_height(self, s):
         """
@@ -348,7 +409,7 @@ def string_width_height(self, s):
         """
         if not len(s):
             return 0, 0
-        totalw = 0
+        total_width = 0
         namelast = None
         miny = 1e9
         maxy = 0
@@ -356,35 +417,21 @@ def string_width_height(self, s):
             if c == '\n':
                 continue
             wx, name, bbox = self._metrics[ord(c)]
+
+            total_width += wx + self._kern.get((namelast, name), 0)
             l, b, w, h = bbox
+            miny = min(miny, b)
+            maxy = max(maxy, b + h)
 
-            # find the width with kerning
-            try:
-                kp = self._kern[(namelast, name)]
-            except KeyError:
-                kp = 0
-            totalw += wx + kp
-
-            # find the max y
-            thismax = b + h
-            if thismax > maxy:
-                maxy = thismax
-
-            # find the min y
-            thismin = b
-            if thismin < miny:
-                miny = thismin
             namelast = name
 
-        return totalw, maxy - miny
+        return total_width, maxy - miny
 
     def get_str_bbox_and_descent(self, s):
-        """
-        Return the string bounding box
-        """
+        """Return the string bounding box and the maximal descent."""
         if not len(s):
-            return 0, 0, 0, 0
-        totalw = 0
+            return 0, 0, 0, 0, 0
+        total_width = 0
         namelast = None
         miny = 1e9
         maxy = 0
@@ -396,79 +443,51 @@ def get_str_bbox_and_descent(self, s):
                 continue
             name = uni2type1.get(ord(c), 'question')
             try:
-                wx, bbox = self._metrics_by_name[name]
+                wx, _, bbox = self._metrics_by_name[name]
             except KeyError:
                 name = 'question'
-                wx, bbox = self._metrics_by_name[name]
+                wx, _, bbox = self._metrics_by_name[name]
+            total_width += wx + self._kern.get((namelast, name), 0)
             l, b, w, h = bbox
-            if l < left:
-                left = l
-            # find the width with kerning
-            try:
-                kp = self._kern[(namelast, name)]
-            except KeyError:
-                kp = 0
-            totalw += wx + kp
-
-            # find the max y
-            thismax = b + h
-            if thismax > maxy:
-                maxy = thismax
-
-            # find the min y
-            thismin = b
-            if thismin < miny:
-                miny = thismin
+            left = min(left, l)
+            miny = min(miny, b)
+            maxy = max(maxy, b + h)
+
             namelast = name
 
-        return left, miny, totalw, maxy - miny, -miny
+        return left, miny, total_width, maxy - miny, -miny
 
     def get_str_bbox(self, s):
-        """
-        Return the string bounding box
-        """
+        """Return the string bounding box."""
         return self.get_str_bbox_and_descent(s)[:4]
 
     def get_name_char(self, c, isord=False):
-        """
-        Get the name of the character, i.e., ';' is 'semicolon'
-        """
+        """Get the name of the character, i.e., ';' is 'semicolon'."""
         if not isord:
             c = ord(c)
-        wx, name, bbox = self._metrics[c]
-        return name
+        return self._metrics[c].name
 
     def get_width_char(self, c, isord=False):
         """
-        Get the width of the character from the character metric WX
-        field
+        Get the width of the character from the character metric WX field.
         """
         if not isord:
             c = ord(c)
-        wx, name, bbox = self._metrics[c]
-        return wx
+        return self._metrics[c].width
 
     def get_width_from_char_name(self, name):
-        """
-        Get the width of the character from a type1 character name
-        """
-        wx, bbox = self._metrics_by_name[name]
-        return wx
+        """Get the width of the character from a type1 character name."""
+        return self._metrics_by_name[name].width
 
     def get_height_char(self, c, isord=False):
-        """
-        Get the height of character *c* from the bounding box.  This
-        is the ink height (space is 0)
-        """
+        """Get the bounding box (ink) height of character *c* (space is 0)."""
         if not isord:
             c = ord(c)
-        wx, name, bbox = self._metrics[c]
-        return bbox[-1]
+        return self._metrics[c].bbox[-1]
 
     def get_kern_dist(self, c1, c2):
         """
-        Return the kerning pair distance (possibly 0) for chars *c1*
-        and *c2*
+        Return the kerning pair distance (possibly 0) for chars *c1* and *c2*.
         """
         name1, name2 = self.get_name_char(c1), self.get_name_char(c2)
         return self.get_kern_dist_from_name(name1, name2)
@@ -476,23 +495,23 @@ def get_kern_dist(self, c1, c2):
     def get_kern_dist_from_name(self, name1, name2):
         """
         Return the kerning pair distance (possibly 0) for chars
-        *name1* and *name2*
+        *name1* and *name2*.
         """
         return self._kern.get((name1, name2), 0)
 
     def get_fontname(self):
-        "Return the font name, e.g., 'Times-Roman'"
+        """Return the font name, e.g., 'Times-Roman'."""
         return self._header[b'FontName']
 
     def get_fullname(self):
-        "Return the font full name, e.g., 'Times-Roman'"
+        """Return the font full name, e.g., 'Times-Roman'."""
         name = self._header.get(b'FullName')
         if name is None:  # use FontName as a substitute
             name = self._header[b'FontName']
         return name
 
     def get_familyname(self):
-        "Return the font family name, e.g., 'Times'"
+        """Return the font family name, e.g., 'Times'."""
         name = self._header.get(b'FamilyName')
         if name is not None:
             return name
@@ -505,26 +524,27 @@ def get_familyname(self):
 
     @property
     def family_name(self):
+        """The font family name, e.g., 'Times'."""
         return self.get_familyname()
 
     def get_weight(self):
-        "Return the font weight, e.g., 'Bold' or 'Roman'"
+        """Return the font weight, e.g., 'Bold' or 'Roman'."""
         return self._header[b'Weight']
 
     def get_angle(self):
-        "Return the fontangle as float"
+        """Return the fontangle as float."""
         return self._header[b'ItalicAngle']
 
     def get_capheight(self):
-        "Return the cap height as float"
+        """Return the cap height as float."""
         return self._header[b'CapHeight']
 
     def get_xheight(self):
-        "Return the xheight as float"
+        """Return the xheight as float."""
         return self._header[b'XHeight']
 
     def get_underline_thickness(self):
-        "Return the underline thickness as float"
+        """Return the underline thickness as float."""
         return self._header[b'UnderlineThickness']
 
     def get_horizontal_stem_width(self):
diff --git a/lib/matplotlib/tests/test_afm.py b/lib/matplotlib/tests/test_afm.py
index eef807b1d3df..25c7a2ad0f92 100644
--- a/lib/matplotlib/tests/test_afm.py
+++ b/lib/matplotlib/tests/test_afm.py
@@ -67,9 +67,9 @@ def test_parse_char_metrics():
          42: (1141.0, 'foo', [40, 60, 800, 360]),
          99: (583.0, 'bar', [40, -10, 543, 210]),
          },
-        {'space': (250.0, [0, 0, 0, 0]),
-         'foo': (1141.0, [40, 60, 800, 360]),
-         'bar': (583.0, [40, -10, 543, 210]),
+        {'space': (250.0, 'space', [0, 0, 0, 0]),
+         'foo': (1141.0, 'foo', [40, 60, 800, 360]),
+         'bar': (583.0, 'bar', [40, -10, 543, 210]),
          })