diff --git a/src/python/geomag/io/iaga2002/IAGA2002Writer.py b/src/python/geomag/io/iaga2002/IAGA2002Writer.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b388043cd679ed2acd3a67023a29ab73f9c3021
--- /dev/null
+++ b/src/python/geomag/io/iaga2002/IAGA2002Writer.py
@@ -0,0 +1,162 @@
+
+from cStringIO import StringIO
+from geomag.io import TimeseriesFactoryException
+import numpy
+import IAGA2002Parser
+import textwrap
+
+
+class IAGA2002Writer(object):
+    """IAGA2002 writer.
+    """
+
+    def __init__(self, empty_value=IAGA2002Parser.NINES):
+        self.empty_value = empty_value
+
+    def write(self, out, timeseries, channels):
+        stats = timeseries[0].stats
+        out.write(self._format_headers(stats, channels))
+        out.write(self._format_comments(stats.comments))
+        out.write(self._format_channels(channels, stats['IAGA CODE']))
+        out.write(self._format_data(timeseries, channels))
+        pass
+
+    def _format_headers(self, stats, channels):
+        values = {}
+        values.update(stats)
+        values['Format'] = 'IAGA-2002'
+        values['Reported'] = ''.join(channels)
+        buf = []
+        for header in (
+                'Format',
+                'Source of Data',
+                'Station Name',
+                'IAGA CODE',
+                'Geodetic Latitude',
+                'Geodetic Longitude',
+                'Elevation',
+                'Reported',
+                'Sensor Orientation',
+                'Digital Sampling',
+                'Data Interval Type',
+                'Data Type'):
+            buf.append(self._format_header(header, values[header]))
+        return ''.join(buf)
+
+    def _format_comments(self, comments):
+        buf = []
+        for comment in comments:
+            buf.append(self._format_comment(comment))
+        return ''.join(buf)
+
+    def _format_header(self, name, value):
+        prefix = ' '
+        suffix = ' |\n'
+        return ''.join((prefix, name.ljust(23), value.ljust(44), suffix))
+
+    def _format_comment(self, comment):
+        buf = []
+        prefix = ' # '
+        suffix = ' |\n'
+        lines = textwrap.wrap(comment, 65)
+        for line in lines:
+            buf.extend((prefix, line.ljust(65), suffix))
+        return ''.join(buf)
+
+    def _format_channels(self, channels, iaga_code):
+        """Format channel header line.
+
+        Parameters
+        ----------
+        channels : sequence
+            list and order of channel values to output.
+        iaga_code : str
+            observatory code, which is prefixed to channel name in output.
+
+        Returns
+        -------
+        str
+            Channel header line as a string (including newline)
+        """
+        if len(iaga_code) != 3:
+            raise TimeseriesFactoryException(
+                    'iaga_code "{}" is not 3 characters'.format(iaga_code))
+        if len(channels) != 4:
+            raise TimeseriesFactoryException(
+                    'more than 4 channels {}'.format(channels))
+        buf = ['DATE       TIME         DOY']
+        for channel in channels:
+            if len(channel) != 1:
+                raise TimeseriesFactoryException(
+                        'channel "{}" is not 1 character'.format(channel))
+            buf.append('     %s%s ' % (iaga_code, channel))
+        buf.append('  |\n')
+        return ''.join(buf)
+
+    def _format_data(self, timeseries, channels):
+        """Format all data lines.
+
+        Parameters
+        ----------
+        timeseries : obspy.core.Stream
+            stream containing traces with channel listed in channels
+        channels : sequence
+            list and order of channel values to output.
+        """
+        buf = []
+        traces = [timeseries.select(channel=c)[0] for c in channels]
+        starttime = traces[0].stats.starttime
+        delta = traces[0].stats.delta
+        for i in xrange(len(traces[0].data)):
+            buf.append(self._format_values(
+                starttime + i * delta,
+                (t.data[i] for t in traces)))
+        return ''.join(buf)
+
+    def _format_values(self, time, values):
+        """Format one line of data values.
+
+        Parameters
+        ----------
+        time : UTCDateTime
+            timestamp for values
+        values : sequence
+            list and order of channel values to output.
+            if value is NaN, self.empty_value is output in its place.
+
+        Returns
+        -------
+        unicode
+            Formatted line containing values.
+        """
+        buf = []
+        buf.extend((
+                time.strftime('%Y-%m-%d %H:%M:%S.'),
+                '{:0>3.0f}'.format(round(time.microsecond / 1000, 0)),
+                time.strftime(' %j   ')))
+        for value in values:
+            if numpy.isnan(value):
+                value = self.empty_value
+            buf.append('{:10.2f}'.format(value))
+        buf.append('\n')
+        return ''.join(buf)
+
+    @classmethod
+    def format(self, timeseries, channels):
+        """Get an IAGA2002 formatted string.
+
+        Calls write() with a StringIO, and returns the output.
+
+        Parameters
+        ----------
+        timeseries : obspy.core.Stream
+
+        Returns
+        -------
+        unicode
+          IAGA2002 formatted string.
+        """
+        out = StringIO()
+        writer = IAGA2002Writer()
+        writer.write(out, timeseries, channels)
+        return out.getvalue()
diff --git a/src/python/geomag/io/iaga2002/__init__.py b/src/python/geomag/io/iaga2002/__init__.py
index 6525f026e274099f5fff570aa185eba0956f0498..bc43aac9b02eb734afb8d1642280b7cbe801ab8e 100644
--- a/src/python/geomag/io/iaga2002/__init__.py
+++ b/src/python/geomag/io/iaga2002/__init__.py
@@ -6,11 +6,13 @@ Based on documentation at:
 
 from IAGA2002Factory import IAGA2002Factory
 from IAGA2002Parser import IAGA2002Parser
+from IAGA2002Writer import IAGA2002Writer
 from MagWebFactory import MagWebFactory
 
 
 __all__ = [
     'IAGA2002Factory',
     'IAGA2002Parser',
+    'IAGA2002Writer',
     'MagWebFactory'
 ]