Repository: pandas-dev/pandas

Issue:
ENH: Add dateutil timezone support
This enhancement allows Pandas to use `dateutil` timezones as well as the default `pytz` ones. The `Timestamp`'s would be inter-operable.

It has required a bit of coding in `tslib.pyx` to discriminate between `pytz` and `dateutil` tzinfo objects and load the appropriate data into the cache.

I'll create PR for this.



Discussion:
@prossahl after thinking it over, unless you have a specific reason in mind why this is necessary (as opposed to using a pytz timezone instead), I don't think we are going to incorporate this. Just wanted to give you a heads up before you spend more time on this.

I don't see the problem honestly. But I haven't looked at the PR yet, doing so now

@wesm adds complexity and it's not clear what it adds in usefulness?

It does add complexity but what I meant is that in reality you might see cases where `dateutil` time zones crept into `Timestamp` objects and, while pandas would prefer the use of pytz, it would cause interoperability problems. The diff to implement the functionality isn't that large (relative to the rest of `tslib.pyx`, which has already grown into a bit of a jungle and might be due for some refactoring/cleanup at some point).

that's helpful context.

Thanks for the comments. My motive for doing this was that quite a lot of people (including us!) use the `dateutil` library quite extensively. Whilst you can construct a Pandas `Timestamp` with a dateutil time zone any attempt to convert to another `dateutil` time zone will fail:

``` python
>>> ts = pd.Timestamp('2013-08-30 12:00', tz=dateutil.tz.gettz('US/Eastern'))
>>> ts.astimezone(tz=dateutil.tz.gettz('US/Pacific'))
Exception AttributeError: "'NoneType' object has no attribute 'toordinal'" in 'pandas.tslib._localize_tso' ignored
<Timestamp: 2013-08-30 16:00:00>
```

This seems to be little dangerous for `dateutil` users. It looks like there are three solutions, either:
(a) Support `dateutil` time zones fully.
(b) Convert `dateutil` time zones internally to `pytz` timezones.
(c) Refuse to accept `dateutil` time zones.

This issue does (a) and seems to be a small change to be able to inter-operate with both the time zone libraries in common use.

An alternative is (b), with some small performance impact, which would mean working out the correspondence between dateutil and pytz. Something like:

``` python
pytz.timezone('/'.join(du_tz._filename.split('/')[-2:]))
```

would work but there are some corner cases like UTC.

I do realise that you may want to remove a dependency on `dateutil` entirely in which case (c) would be the solution but this would be a bit disappointing for those that have lots of other code depending on `dateutil`!

Thank you for laying that out for us - I appreciate you taking the time to
detail that!


Resolving patch:
diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py
--- a/pandas/io/pytables.py
+++ b/pandas/io/pytables.py
@@ -1729,11 +1729,23 @@ def set_atom(self, block, block_items, existing_col, min_itemsize,
             if getattr(rvalues[0], 'tzinfo', None) is not None:
 
                 # if this block has more than one timezone, raise
-                if len(set([r.tzinfo for r in rvalues])) != 1:
-                    raise TypeError(
-                        "too many timezones in this block, create separate "
-                        "data columns"
-                    )
+                try:
+                    # pytz timezones: compare on zone name (to avoid issues with DST being a different zone to STD).
+                    zones = [r.tzinfo.zone for r in rvalues]
+                except:
+                    # dateutil timezones: compare on ==
+                    zones = [r.tzinfo for r in rvalues]
+                    if any(zones[0] != zone_i for zone_i in zones[1:]):
+                        raise TypeError(
+                            "too many timezones in this block, create separate "
+                            "data columns"
+                        )
+                else:
+                    if len(set(zones)) != 1:
+                        raise TypeError(
+                            "too many timezones in this block, create separate "
+                            "data columns"
+                        )
 
                 # convert this column to datetime64[ns] utc, and save the tz
                 index = DatetimeIndex(rvalues)
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- a/pandas/tseries/index.py
+++ b/pandas/tseries/index.py
@@ -1617,7 +1617,7 @@ def _view_like(self, ndarray):
 
     def tz_convert(self, tz):
         """
-        Convert DatetimeIndex from one time zone to another (using pytz)
+        Convert DatetimeIndex from one time zone to another (using pytz/dateutil)
 
         Returns
         -------
@@ -1635,11 +1635,11 @@ def tz_convert(self, tz):
 
     def tz_localize(self, tz, infer_dst=False):
         """
-        Localize tz-naive DatetimeIndex to given time zone (using pytz)
+        Localize tz-naive DatetimeIndex to given time zone (using pytz/dateutil)
 
         Parameters
         ----------
-        tz : string or pytz.timezone
+        tz : string or pytz.timezone or dateutil.tz.tzfile
             Time zone for time. Corresponding timestamps would be converted to
             time zone of the TimeSeries
         infer_dst : boolean, default False
@@ -1666,7 +1666,7 @@ def indexer_at_time(self, time, asof=False):
         Parameters
         ----------
         time : datetime.time or string
-        tz : string or pytz.timezone
+        tz : string or pytz.timezone or dateutil.tz.tzfile
             Time zone for time. Corresponding timestamps would be converted to
             time zone of the TimeSeries
 
@@ -1701,7 +1701,7 @@ def indexer_between_time(self, start_time, end_time, include_start=True,
         end_time : datetime.time or string
         include_start : boolean, default True
         include_end : boolean, default True
-        tz : string or pytz.timezone, default None
+        tz : string or pytz.timezone or dateutil.tz.tzfile, default None
 
         Returns
         -------
diff --git a/pandas/tseries/tools.py b/pandas/tseries/tools.py
--- a/pandas/tseries/tools.py
+++ b/pandas/tseries/tools.py
@@ -57,9 +57,7 @@ def _infer(a, b):
 
 
 def _maybe_get_tz(tz, date=None):
-    if isinstance(tz, compat.string_types):
-        import pytz
-        tz = pytz.timezone(tz)
+    tz = tslib.maybe_get_tz(tz)
     if com.is_integer(tz):
         import pytz
         tz = pytz.FixedOffset(tz / 60)
@@ -71,6 +69,7 @@ def _maybe_get_tz(tz, date=None):
 
     return tz
 
+
 def _guess_datetime_format(dt_str, dayfirst=False,
                            dt_str_parse=compat.parse_date,
                            dt_str_split=_DATEUTIL_LEXER_SPLIT):


Test patch:
diff --git a/pandas/io/tests/test_json/test_ujson.py b/pandas/io/tests/test_json/test_ujson.py
--- a/pandas/io/tests/test_json/test_ujson.py
+++ b/pandas/io/tests/test_json/test_ujson.py
@@ -25,6 +25,7 @@
                            assert_array_almost_equal_nulp,
                            assert_approx_equal)
 import pytz
+import dateutil
 from pandas import DataFrame, Series, Index, NaT, DatetimeIndex
 import pandas.util.testing as tm
 
@@ -361,7 +362,9 @@ def test_encodeTimeConversion(self):
             datetime.time(),
             datetime.time(1, 2, 3),
             datetime.time(10, 12, 15, 343243),
-            datetime.time(10, 12, 15, 343243, pytz.utc)]
+            datetime.time(10, 12, 15, 343243, pytz.utc),
+#             datetime.time(10, 12, 15, 343243, dateutil.tz.gettz('UTC')),  # this segfaults! No idea why.
+            ]
         for test in tests:
             output = ujson.encode(test)
             expected = '"%s"' % test.isoformat()
diff --git a/pandas/io/tests/test_pytables.py b/pandas/io/tests/test_pytables.py
--- a/pandas/io/tests/test_pytables.py
+++ b/pandas/io/tests/test_pytables.py
@@ -1991,7 +1991,7 @@ def test_unimplemented_dtypes_table_columns(self):
             # this fails because we have a date in the object block......
             self.assertRaises(TypeError, store.append, 'df_unimplemented', df)
 
-    def test_append_with_timezones(self):
+    def test_append_with_timezones_pytz(self):
 
         from datetime import timedelta
 
@@ -2020,7 +2020,8 @@ def compare(a,b):
             compare(store.select('df_tz',where=Term('A>=df.A[3]')),df[df.A>=df.A[3]])
 
             _maybe_remove(store, 'df_tz')
-            df = DataFrame(dict(A = Timestamp('20130102',tz='US/Eastern'), B = Timestamp('20130103',tz='US/Eastern')),index=range(5))
+            # ensure we include dates in DST and STD time here.
+            df = DataFrame(dict(A = Timestamp('20130102',tz='US/Eastern'), B = Timestamp('20130603',tz='US/Eastern')),index=range(5))
             store.append('df_tz',df)
             result = store['df_tz']
             compare(result,df)
@@ -2057,6 +2058,78 @@ def compare(a,b):
             result = store.select('df')
             assert_frame_equal(result,df)
 
+    def test_append_with_timezones_dateutil(self):
+
+        from datetime import timedelta
+
+        try:
+            import dateutil
+        except ImportError:
+            raise nose.SkipTest
+
+        def compare(a, b):
+            tm.assert_frame_equal(a, b)
+
+            # compare the zones on each element
+            for c in a.columns:
+                for i in a.index:
+                    a_e = a[c][i]
+                    b_e = b[c][i]
+                    if not (a_e == b_e and a_e.tz == b_e.tz):
+                        raise AssertionError("invalid tz comparsion [%s] [%s]" % (a_e, b_e))
+
+        # as columns
+        with ensure_clean_store(self.path) as store:
+
+            _maybe_remove(store, 'df_tz')
+            df = DataFrame(dict(A=[ Timestamp('20130102 2:00:00', tz=dateutil.tz.gettz('US/Eastern')) + timedelta(hours=1) * i for i in range(5) ]))
+            store.append('df_tz', df, data_columns=['A'])
+            result = store['df_tz']
+            compare(result, df)
+            assert_frame_equal(result, df)
+
+            # select with tz aware
+            compare(store.select('df_tz', where=Term('A>=df.A[3]')), df[df.A >= df.A[3]])
+
+            _maybe_remove(store, 'df_tz')
+            # ensure we include dates in DST and STD time here.
+            df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130603', tz=dateutil.tz.gettz('US/Eastern'))), index=range(5))
+            store.append('df_tz', df)
+            result = store['df_tz']
+            compare(result, df)
+            assert_frame_equal(result, df)
+
+            _maybe_remove(store, 'df_tz')
+            df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130102', tz=dateutil.tz.gettz('EET'))), index=range(5))
+            self.assertRaises(TypeError, store.append, 'df_tz', df)
+
+            # this is ok
+            _maybe_remove(store, 'df_tz')
+            store.append('df_tz', df, data_columns=['A', 'B'])
+            result = store['df_tz']
+            compare(result, df)
+            assert_frame_equal(result, df)
+
+            # can't append with diff timezone
+            df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130102', tz=dateutil.tz.gettz('CET'))), index=range(5))
+            self.assertRaises(ValueError, store.append, 'df_tz', df)
+
+        # as index
+        with ensure_clean_store(self.path) as store:
+
+            # GH 4098 example
+            df = DataFrame(dict(A=Series(lrange(3), index=date_range('2000-1-1', periods=3, freq='H', tz=dateutil.tz.gettz('US/Eastern')))))
+
+            _maybe_remove(store, 'df')
+            store.put('df', df)
+            result = store.select('df')
+            assert_frame_equal(result, df)
+
+            _maybe_remove(store, 'df')
+            store.append('df', df)
+            result = store.select('df')
+            assert_frame_equal(result, df)
+
     def test_store_timezone(self):
         # GH2852
         # issue storing datetime.date with a timezone as it resets when read back in a new timezone
diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py
--- a/pandas/tests/test_format.py
+++ b/pandas/tests/test_format.py
@@ -92,6 +92,13 @@ def _skip_if_no_pytz():
     except ImportError:
         raise nose.SkipTest("pytz not installed")
 
+def _skip_if_no_dateutil():
+    try:
+        import dateutil
+    except ImportError:
+        raise nose.SkipTest("dateutil not installed")
+
+
 class TestDataFrameFormatting(tm.TestCase):
     _multiprocess_can_split_ = True
 
@@ -2922,7 +2929,7 @@ def test_no_tz(self):
         ts_nanos_micros = Timestamp(1200)
         self.assertEqual(str(ts_nanos_micros), "1970-01-01 00:00:00.000001200")
 
-    def test_tz(self):
+    def test_tz_pytz(self):
         _skip_if_no_pytz()
 
         import pytz
@@ -2936,6 +2943,20 @@ def test_tz(self):
         dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=pytz.utc)
         self.assertEqual(str(dt_datetime_us), str(Timestamp(dt_datetime_us)))
 
+    def test_tz_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+        utc = dateutil.tz.gettz('UTC')
+
+        dt_date = datetime(2013, 1, 2, tzinfo=utc)
+        self.assertEqual(str(dt_date), str(Timestamp(dt_date)))
+
+        dt_datetime = datetime(2013, 1, 2, 12, 1, 3, tzinfo=utc)
+        self.assertEqual(str(dt_datetime), str(Timestamp(dt_datetime)))
+
+        dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=utc)
+        self.assertEqual(str(dt_datetime_us), str(Timestamp(dt_datetime_us)))
+
 if __name__ == '__main__':
     import nose
     nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py
--- a/pandas/tests/test_series.py
+++ b/pandas/tests/test_series.py
@@ -47,6 +47,12 @@ def _skip_if_no_pytz():
     except ImportError:
         raise nose.SkipTest("pytz not installed")
 
+def _skip_if_no_dateutil():
+    try:
+        import dateutil
+    except ImportError:
+        raise nose.SkipTest("dateutil not installed")
+
 #------------------------------------------------------------------------------
 # Series test cases
 
@@ -4573,7 +4579,7 @@ def test_getitem_setitem_datetimeindex(self):
         result["1990-01-02"] = ts[24:48]
         assert_series_equal(result, ts)
 
-    def test_getitem_setitem_datetime_tz(self):
+    def test_getitem_setitem_datetime_tz_pytz(self):
         _skip_if_no_pytz();
         from pytz import timezone as tz
 
@@ -4608,6 +4614,39 @@ def test_getitem_setitem_datetime_tz(self):
         result[date] = ts[4]
         assert_series_equal(result, ts)
 
+
+    def test_getitem_setitem_datetime_tz_dateutil(self):
+        _skip_if_no_dateutil();
+        from dateutil.tz import gettz as tz
+
+        from pandas import date_range
+        N = 50
+        # testing with timezone, GH #2785
+        rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern')
+        ts = Series(np.random.randn(N), index=rng)
+
+        # also test Timestamp tz handling, GH #2789
+        result = ts.copy()
+        result["1990-01-01 09:00:00+00:00"] = 0
+        result["1990-01-01 09:00:00+00:00"] = ts[4]
+        assert_series_equal(result, ts)
+
+        result = ts.copy()
+        result["1990-01-01 03:00:00-06:00"] = 0
+        result["1990-01-01 03:00:00-06:00"] = ts[4]
+        assert_series_equal(result, ts)
+
+        # repeat with datetimes
+        result = ts.copy()
+        result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0
+        result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4]
+        assert_series_equal(result, ts)
+
+        result = ts.copy()
+        result[datetime(1990, 1, 1, 3, tzinfo=tz('US/Central'))] = 0
+        result[datetime(1990, 1, 1, 3, tzinfo=tz('US/Central'))] = ts[4]
+        assert_series_equal(result, ts)
+
     def test_getitem_setitem_periodindex(self):
         from pandas import period_range
         N = 50
diff --git a/pandas/tseries/tests/test_daterange.py b/pandas/tseries/tests/test_daterange.py
--- a/pandas/tseries/tests/test_daterange.py
+++ b/pandas/tseries/tests/test_daterange.py
@@ -24,6 +24,12 @@ def _skip_if_no_pytz():
     except ImportError:
         raise nose.SkipTest("pytz not installed")
 
+def _skip_if_no_dateutil():
+    try:
+        import dateutil
+    except ImportError:
+        raise nose.SkipTest("dateutil not installed")
+
 
 def _skip_if_no_cday():
     if datetools.cday is None:
@@ -291,6 +297,11 @@ def test_summary_pytz(self):
         import pytz
         bdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary()
 
+    def test_summary_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+        bdate_range('1/1/2005', '1/1/2009', tz=dateutil.tz.gettz('UTC')).summary()
+
     def test_misc(self):
         end = datetime(2009, 5, 13)
         dr = bdate_range(end=end, periods=20)
@@ -354,7 +365,7 @@ def test_range_bug(self):
         exp_values = [start + i * offset for i in range(5)]
         self.assert_numpy_array_equal(result, DatetimeIndex(exp_values))
 
-    def test_range_tz(self):
+    def test_range_tz_pytz(self):
         # GH 2906
         _skip_if_no_pytz()
         from pytz import timezone as tz
@@ -377,7 +388,30 @@ def test_range_tz(self):
         self.assertEqual(dr[0], start)
         self.assertEqual(dr[2], end)
 
-    def test_month_range_union_tz(self):
+    def test_range_tz_dateutil(self):
+        # GH 2906
+        _skip_if_no_dateutil()
+        from dateutil.tz import gettz as tz
+
+        start = datetime(2011, 1, 1, tzinfo=tz('US/Eastern'))
+        end = datetime(2011, 1, 3, tzinfo=tz('US/Eastern'))
+
+        dr = date_range(start=start, periods=3)
+        self.assert_(dr.tz == tz('US/Eastern'))
+        self.assert_(dr[0] == start)
+        self.assert_(dr[2] == end)
+
+        dr = date_range(end=end, periods=3)
+        self.assert_(dr.tz == tz('US/Eastern'))
+        self.assert_(dr[0] == start)
+        self.assert_(dr[2] == end)
+
+        dr = date_range(start=start, end=end)
+        self.assert_(dr.tz == tz('US/Eastern'))
+        self.assert_(dr[0] == start)
+        self.assert_(dr[2] == end)
+
+    def test_month_range_union_tz_pytz(self):
         _skip_if_no_pytz()
         from pytz import timezone
         tz = timezone('US/Eastern')
@@ -393,6 +427,22 @@ def test_month_range_union_tz(self):
 
         early_dr.union(late_dr)
 
+    def test_month_range_union_tz_dateutil(self):
+        _skip_if_no_dateutil()
+        from dateutil.tz import gettz as timezone
+        tz = timezone('US/Eastern')
+
+        early_start = datetime(2011, 1, 1)
+        early_end = datetime(2011, 3, 1)
+
+        late_start = datetime(2011, 3, 1)
+        late_end = datetime(2011, 5, 1)
+
+        early_dr = date_range(start=early_start, end=early_end, tz=tz, freq=datetools.monthEnd)
+        late_dr = date_range(start=late_start, end=late_end, tz=tz, freq=datetools.monthEnd)
+
+        early_dr.union(late_dr)
+
     def test_range_closed(self):
         begin = datetime(2011, 1, 1)
         end = datetime(2014, 1, 1)
@@ -580,6 +630,11 @@ def test_summary_pytz(self):
         import pytz
         cdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary()
 
+    def test_summary_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+        cdate_range('1/1/2005', '1/1/2009', tz=dateutil.tz.gettz('UTC')).summary()
+
     def test_misc(self):
         end = datetime(2009, 5, 13)
         dr = cdate_range(end=end, periods=20)
diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py
--- a/pandas/tseries/tests/test_period.py
+++ b/pandas/tseries/tests/test_period.py
@@ -83,6 +83,16 @@ def test_timestamp_tz_arg(self):
         self.assertEqual(p.tz,
                          pytz.timezone('Europe/Brussels').normalize(p).tzinfo)
 
+    def test_timestamp_tz_arg_dateutil(self):
+        import dateutil
+        p = Period('1/1/2005', freq='M').to_timestamp(tz=dateutil.tz.gettz('Europe/Brussels'))
+        self.assertEqual(p.tz, dateutil.tz.gettz('Europe/Brussels'))
+
+    def test_timestamp_tz_arg_dateutil_from_string(self):
+        import dateutil
+        p = Period('1/1/2005', freq='M').to_timestamp(tz='dateutil/Europe/Brussels')
+        self.assertEqual(p.tz, dateutil.tz.gettz('Europe/Brussels'))
+
     def test_period_constructor(self):
         i1 = Period('1/1/2005', freq='M')
         i2 = Period('Jan 2005')
diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py
--- a/pandas/tseries/tests/test_timeseries.py
+++ b/pandas/tseries/tests/test_timeseries.py
@@ -41,6 +41,12 @@
 from numpy.testing.decorators import slow
 
 
+def _skip_if_no_dateutil():
+    try:
+        import dateutil
+    except ImportError:
+        raise nose.SkipTest("dateutil not installed")
+
 def _skip_if_no_pytz():
     try:
         import pytz
@@ -400,6 +406,28 @@ def test_timestamp_to_datetime(self):
         self.assertEqual(stamp, dtval)
         self.assertEqual(stamp.tzinfo, dtval.tzinfo)
 
+    def test_timestamp_to_datetime_explicit_pytz(self):
+        _skip_if_no_pytz()
+        import pytz
+        rng = date_range('20090415', '20090519',
+                         tz=pytz.timezone('US/Eastern'))
+
+        stamp = rng[0]
+        dtval = stamp.to_pydatetime()
+        self.assertEquals(stamp, dtval)
+        self.assertEquals(stamp.tzinfo, dtval.tzinfo)
+
+    def test_timestamp_to_datetime_explicit_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+        rng = date_range('20090415', '20090519',
+                         tz=dateutil.tz.gettz('US/Eastern'))
+
+        stamp = rng[0]
+        dtval = stamp.to_pydatetime()
+        self.assertEquals(stamp, dtval)
+        self.assertEquals(stamp.tzinfo, dtval.tzinfo)
+
     def test_index_convert_to_datetime_array(self):
         _skip_if_no_pytz()
 
@@ -419,6 +447,46 @@ def _check_rng(rng):
         _check_rng(rng_eastern)
         _check_rng(rng_utc)
 
+    def test_index_convert_to_datetime_array_explicit_pytz(self):
+        _skip_if_no_pytz()
+        import pytz
+
+        def _check_rng(rng):
+            converted = rng.to_pydatetime()
+            tm.assert_isinstance(converted, np.ndarray)
+            for x, stamp in zip(converted, rng):
+                tm.assert_isinstance(x, datetime)
+                self.assertEquals(x, stamp.to_pydatetime())
+                self.assertEquals(x.tzinfo, stamp.tzinfo)
+
+        rng = date_range('20090415', '20090519')
+        rng_eastern = date_range('20090415', '20090519', tz=pytz.timezone('US/Eastern'))
+        rng_utc = date_range('20090415', '20090519', tz=pytz.utc)
+
+        _check_rng(rng)
+        _check_rng(rng_eastern)
+        _check_rng(rng_utc)
+
+    def test_index_convert_to_datetime_array_explicit_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+
+        def _check_rng(rng):
+            converted = rng.to_pydatetime()
+            tm.assert_isinstance(converted, np.ndarray)
+            for x, stamp in zip(converted, rng):
+                tm.assert_isinstance(x, datetime)
+                self.assertEquals(x, stamp.to_pydatetime())
+                self.assertEquals(x.tzinfo, stamp.tzinfo)
+
+        rng = date_range('20090415', '20090519')
+        rng_eastern = date_range('20090415', '20090519', tz=dateutil.tz.gettz('US/Eastern'))
+        rng_utc = date_range('20090415', '20090519', tz=dateutil.tz.gettz('UTC'))
+
+        _check_rng(rng)
+        _check_rng(rng_eastern)
+        _check_rng(rng_utc)
+
     def test_ctor_str_intraday(self):
         rng = DatetimeIndex(['1-1-2000 00:00:01'])
         self.assertEqual(rng[0].second, 1)
@@ -1430,7 +1498,7 @@ def test_to_period_microsecond(self):
         self.assertEqual(period[0], Period('2007-01-01 10:11:12.123456Z', 'U'))
         self.assertEqual(period[1], Period('2007-01-01 10:11:13.789123Z', 'U'))
 
-    def test_to_period_tz(self):
+    def test_to_period_tz_pytz(self):
         _skip_if_no_pytz()
         from dateutil.tz import tzlocal
         from pytz import utc as UTC
@@ -1461,6 +1529,68 @@ def test_to_period_tz(self):
         self.assertEqual(result, expected)
         self.assertTrue(ts.to_period().equals(xp))
 
+    def test_to_period_tz_explicit_pytz(self):
+        _skip_if_no_pytz()
+        import pytz
+        from dateutil.tz import tzlocal
+
+        xp = date_range('1/1/2000', '4/1/2000').to_period()
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=pytz.timezone('US/Eastern'))
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=pytz.utc)
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=tzlocal())
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
+    def test_to_period_tz_explicit_dateutil(self):
+        _skip_if_no_dateutil()
+        import dateutil
+        from dateutil.tz import tzlocal
+
+        xp = date_range('1/1/2000', '4/1/2000').to_period()
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=dateutil.tz.gettz('US/Eastern'))
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=dateutil.tz.gettz('UTC'))
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
+        ts = date_range('1/1/2000', '4/1/2000', tz=tzlocal())
+
+        result = ts.to_period()[0]
+        expected = ts[0].to_period()
+
+        self.assert_(result == expected)
+        self.assert_(ts.to_period().equals(xp))
+
     def test_frame_to_period(self):
         K = 5
         from pandas.tseries.period import period_range
@@ -1639,6 +1769,54 @@ def test_append_concat_tz(self):
         appended = rng.append(rng2)
         self.assertTrue(appended.equals(rng3))
 
+    def test_append_concat_tz_explicit_pytz(self):
+        # GH 2938
+        _skip_if_no_pytz()
+        from pytz import timezone as timezone
+
+        rng = date_range('5/8/2012 1:45', periods=10, freq='5T',
+                         tz=timezone('US/Eastern'))
+        rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T',
+                         tz=timezone('US/Eastern'))
+        rng3 = date_range('5/8/2012 1:45', periods=20, freq='5T',
+                         tz=timezone('US/Eastern'))
+        ts = Series(np.random.randn(len(rng)), rng)
+        df = DataFrame(np.random.randn(len(rng), 4), index=rng)
+        ts2 = Series(np.random.randn(len(rng2)), rng2)
+        df2 = DataFrame(np.random.randn(len(rng2), 4), index=rng2)
+
+        result = ts.append(ts2)
+        result_df = df.append(df2)
+        self.assert_(result.index.equals(rng3))
+        self.assert_(result_df.index.equals(rng3))
+
+        appended = rng.append(rng2)
+        self.assert_(appended.equals(rng3))
+
+    def test_append_concat_tz_explicit_dateutil(self):
+        # GH 2938
+        _skip_if_no_dateutil()
+        from dateutil.tz import gettz as timezone
+
+        rng = date_range('5/8/2012 1:45', periods=10, freq='5T',
+                         tz=timezone('US/Eastern'))
+        rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T',
+                         tz=timezone('US/Eastern'))
+        rng3 = date_range('5/8/2012 1:45', periods=20, freq='5T',
+                         tz=timezone('US/Eastern'))
+        ts = Series(np.random.randn(len(rng)), rng)
+        df = DataFrame(np.random.randn(len(rng), 4), index=rng)
+        ts2 = Series(np.random.randn(len(rng2)), rng2)
+        df2 = DataFrame(np.random.randn(len(rng2), 4), index=rng2)
+
+        result = ts.append(ts2)
+        result_df = df.append(df2)
+        self.assert_(result.index.equals(rng3))
+        self.assert_(result_df.index.equals(rng3))
+
+        appended = rng.append(rng2)
+        self.assert_(appended.equals(rng3))
+
     def test_set_dataframe_column_ns_dtype(self):
         x = DataFrame([datetime.now(), datetime.now()])
         self.assertEqual(x[0].dtype, np.dtype('M8[ns]'))
@@ -1817,7 +1995,7 @@ def test_period_resample(self):
         result2 = s.resample('T', kind='period')
         assert_series_equal(result2, expected)
 
-    def test_period_resample_with_local_timezone(self):
+    def test_period_resample_with_local_timezone_pytz(self):
         # GH5430
         _skip_if_no_pytz()
         import pytz
@@ -1838,6 +2016,28 @@ def test_period_resample_with_local_timezone(self):
         expected = pd.Series(1, index=expected_index)
         assert_series_equal(result, expected)
 
+    def test_period_resample_with_local_timezone_dateutil(self):
+        # GH5430
+        _skip_if_no_dateutil()
+        import dateutil
+
+        local_timezone = dateutil.tz.gettz('America/Los_Angeles')
+
+        start = datetime(year=2013, month=11, day=1, hour=0, minute=0, tzinfo=dateutil.tz.gettz('UTC'))
+        # 1 day later
+        end = datetime(year=2013, month=11, day=2, hour=0, minute=0, tzinfo=dateutil.tz.gettz('UTC'))
+
+        index = pd.date_range(start, end, freq='H')
+
+        series = pd.Series(1, index=index)
+        series = series.tz_convert(local_timezone)
+        result = series.resample('D', kind='period')
+        # Create the expected series
+        expected_index = (pd.period_range(start=start, end=end, freq='D') - 1)  # Index is moved back a day with the timezone conversion from UTC to Pacific
+        expected = pd.Series(1, index=expected_index)
+        assert_series_equal(result, expected)
+
+
     def test_pickle(self):
         #GH4606
         from pandas.compat import cPickle
@@ -2727,15 +2927,27 @@ def test_string_index_series_name_converted(self):
 
 class TestTimestamp(tm.TestCase):
 
-    def test_class_ops(self):
+    def test_class_ops_pytz(self):
         _skip_if_no_pytz()
-        import pytz
+        from pytz import timezone
+
+        def compare(x, y):
+            self.assertEqual(int(Timestamp(x).value / 1e9), int(Timestamp(y).value / 1e9))
+
+        compare(Timestamp.now(), datetime.now())
+        compare(Timestamp.now('UTC'), datetime.now(timezone('UTC')))
+        compare(Timestamp.utcnow(), datetime.utcnow())
+        compare(Timestamp.today(), datetime.today())
+
+    def test_class_ops_dateutil(self):
+        _skip_if_no_dateutil()
+        from dateutil.tz import gettz as timezone
 
         def compare(x,y):
             self.assertEqual(int(np.round(Timestamp(x).value/1e9)), int(np.round(Timestamp(y).value/1e9)))
 
         compare(Timestamp.now(),datetime.now())
-        compare(Timestamp.now('UTC'),datetime.now(pytz.timezone('UTC')))
+        compare(Timestamp.now('UTC'), datetime.now(timezone('UTC')))
         compare(Timestamp.utcnow(),datetime.utcnow())
         compare(Timestamp.today(),datetime.today())
 
@@ -2863,6 +3075,53 @@ def test_cant_compare_tz_naive_w_aware(self):
             self.assertFalse(a == b.to_pydatetime())
             self.assertFalse(a.to_pydatetime() == b)
 
+    def test_cant_compare_tz_naive_w_aware_explicit_pytz(self):
+        _skip_if_no_pytz()
+        from pytz import utc
+        # #1404
+        a = Timestamp('3/12/2012')
+        b = Timestamp('3/12/2012', tz=utc)
+
+        self.assertRaises(Exception, a.__eq__, b)
+        self.assertRaises(Exception, a.__ne__, b)
+        self.assertRaises(Exception, a.__lt__, b)
+        self.assertRaises(Exception, a.__gt__, b)
+        self.assertRaises(Exception, b.__eq__, a)
+        self.assertRaises(Exception, b.__ne__, a)
+        self.assertRaises(Exception, b.__lt__, a)
+        self.assertRaises(Exception, b.__gt__, a)
+
+        if sys.version_info < (3, 3):
+            self.assertRaises(Exception, a.__eq__, b.to_pydatetime())
+            self.assertRaises(Exception, a.to_pydatetime().__eq__, b)
+        else:
+            self.assertFalse(a == b.to_pydatetime())
+            self.assertFalse(a.to_pydatetime() == b)
+
+    def test_cant_compare_tz_naive_w_aware_dateutil(self):
+        _skip_if_no_dateutil()
+        from dateutil.tz import gettz
+        utc = gettz('UTC')
+        # #1404
+        a = Timestamp('3/12/2012')
+        b = Timestamp('3/12/2012', tz=utc)
+
+        self.assertRaises(Exception, a.__eq__, b)
+        self.assertRaises(Exception, a.__ne__, b)
+        self.assertRaises(Exception, a.__lt__, b)
+        self.assertRaises(Exception, a.__gt__, b)
+        self.assertRaises(Exception, b.__eq__, a)
+        self.assertRaises(Exception, b.__ne__, a)
+        self.assertRaises(Exception, b.__lt__, a)
+        self.assertRaises(Exception, b.__gt__, a)
+
+        if sys.version_info < (3, 3):
+            self.assertRaises(Exception, a.__eq__, b.to_pydatetime())
+            self.assertRaises(Exception, a.to_pydatetime().__eq__, b)
+        else:
+            self.assertFalse(a == b.to_pydatetime())
+            self.assertFalse(a.to_pydatetime() == b)
+
     def test_delta_preserve_nanos(self):
         val = Timestamp(long(1337299200000000123))
         result = val + timedelta(1)
diff --git a/pandas/tseries/tests/test_timezones.py b/pandas/tseries/tests/test_timezones.py
--- a/pandas/tseries/tests/test_timezones.py
+++ b/pandas/tseries/tests/test_timezones.py
@@ -2,6 +2,7 @@
 from datetime import datetime, time, timedelta, tzinfo, date
 import sys
 import os
+import unittest
 import nose
 
 import numpy as np
@@ -11,6 +12,7 @@
                     date_range, Timestamp)
 
 from pandas import DatetimeIndex, Int64Index, to_datetime, NaT
+from pandas import tslib
 
 import pandas.core.datetools as datetools
 import pandas.tseries.offsets as offsets
@@ -38,11 +40,22 @@ def _skip_if_no_pytz():
     except ImportError:
         raise nose.SkipTest("pytz not installed")
 
+def _skip_if_no_dateutil():
+    try:
+        import dateutil
+    except ImportError:
+        raise nose.SkipTest
+
 try:
     import pytz
 except ImportError:
     pass
 
+try:
+    import dateutil
+except ImportError:
+    pass
+
 
 class FixedOffset(tzinfo):
     """Fixed offset in minutes east from UTC."""
@@ -64,20 +77,44 @@ def dst(self, dt):
 fixed_off_no_name = FixedOffset(-330, None)
 
 
-class TestTimeZoneSupport(tm.TestCase):
+class TestTimeZoneSupportPytz(tm.TestCase):
     _multiprocess_can_split_ = True
 
     def setUp(self):
         _skip_if_no_pytz()
 
+    def tz(self, tz):
+        ''' Construct a timezone object from a string. Overridden in subclass to parameterize tests. '''
+        return pytz.timezone(tz)
+
+    def tzstr(self, tz):
+        ''' Construct a timezone string from a string. Overridden in subclass to parameterize tests. '''
+        return tz
+
+    def localize(self, tz, x):
+        return tz.localize(x)
+
+    def cmptz(self, tz1, tz2):
+        ''' Compare two timezones. Overridden in subclass to parameterize tests. '''
+        return tz1.zone == tz2.zone
+
     def test_utc_to_local_no_modify(self):
         rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc')
-        rng_eastern = rng.tz_convert('US/Eastern')
+        rng_eastern = rng.tz_convert(self.tzstr('US/Eastern'))
+
+        # Values are unmodified
+        self.assert_(np.array_equal(rng.asi8, rng_eastern.asi8))
+
+        self.assert_(self.cmptz(rng_eastern.tz, self.tz('US/Eastern')))
+
+    def test_utc_to_local_no_modify_explicit(self):
+        rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc')
+        rng_eastern = rng.tz_convert(self.tz('US/Eastern'))
 
         # Values are unmodified
         self.assert_numpy_array_equal(rng.asi8, rng_eastern.asi8)
 
-        self.assertEqual(rng_eastern.tz, pytz.timezone('US/Eastern'))
+        self.assertEqual(rng_eastern.tz, self.tz('US/Eastern'))
 
 
     def test_localize_utc_conversion(self):
@@ -87,20 +124,43 @@ def test_localize_utc_conversion(self):
 
         rng = date_range('3/10/2012', '3/11/2012', freq='30T')
 
-        converted = rng.tz_localize('US/Eastern')
+        converted = rng.tz_localize(self.tzstr('US/Eastern'))
         expected_naive = rng + offsets.Hour(5)
         self.assert_numpy_array_equal(converted.asi8, expected_naive.asi8)
 
         # DST ambiguity, this should fail
         rng = date_range('3/11/2012', '3/12/2012', freq='30T')
         # Is this really how it should fail??
-        self.assertRaises(NonExistentTimeError, rng.tz_localize, 'US/Eastern')
+        self.assertRaises(NonExistentTimeError, rng.tz_localize, self.tzstr('US/Eastern'))
+
+    def test_localize_utc_conversion_explicit(self):
+        # Localizing to time zone should:
+        #  1) check for DST ambiguities
+        #  2) convert to UTC
+
+        rng = date_range('3/10/2012', '3/11/2012', freq='30T')
+        converted = rng.tz_localize(self.tz('US/Eastern'))
+        expected_naive = rng + offsets.Hour(5)
+        self.assert_(np.array_equal(converted.asi8, expected_naive.asi8))
+
+        # DST ambiguity, this should fail
+        rng = date_range('3/11/2012', '3/12/2012', freq='30T')
+        # Is this really how it should fail??
+        self.assertRaises(NonExistentTimeError, rng.tz_localize, self.tz('US/Eastern'))
 
     def test_timestamp_tz_localize(self):
         stamp = Timestamp('3/11/2012 04:00')
 
-        result = stamp.tz_localize('US/Eastern')
-        expected = Timestamp('3/11/2012 04:00', tz='US/Eastern')
+        result = stamp.tz_localize(self.tzstr('US/Eastern'))
+        expected = Timestamp('3/11/2012 04:00', tz=self.tzstr('US/Eastern'))
+        self.assertEqual(result.hour, expected.hour)
+        self.assertEqual(result, expected)
+
+    def test_timestamp_tz_localize_explicit(self):
+        stamp = Timestamp('3/11/2012 04:00')
+
+        result = stamp.tz_localize(self.tz('US/Eastern'))
+        expected = Timestamp('3/11/2012 04:00', tz=self.tz('US/Eastern'))
         self.assertEqual(result.hour, expected.hour)
         self.assertEqual(result, expected)
 
@@ -108,12 +168,22 @@ def test_timestamp_constructed_by_date_and_tz(self):
         # Fix Issue 2993, Timestamp cannot be constructed by datetime.date
         # and tz correctly
 
-        result = Timestamp(date(2012, 3, 11), tz='US/Eastern')
+        result = Timestamp(date(2012, 3, 11), tz=self.tzstr('US/Eastern'))
 
-        expected = Timestamp('3/11/2012', tz='US/Eastern')
+        expected = Timestamp('3/11/2012', tz=self.tzstr('US/Eastern'))
         self.assertEqual(result.hour, expected.hour)
         self.assertEqual(result, expected)
 
+    def test_timestamp_constructed_by_date_and_tz_explicit(self):
+        # Fix Issue 2993, Timestamp cannot be constructed by datetime.date
+        # and tz correctly
+
+        result = Timestamp(date(2012, 3, 11), tz=self.tz('US/Eastern'))
+
+        expected = Timestamp('3/11/2012', tz=self.tz('US/Eastern'))
+        self.assertEquals(result.hour, expected.hour)
+        self.assertEquals(result, expected)
+
     def test_timestamp_to_datetime_tzoffset(self):
         # tzoffset
         from dateutil.tz import tzoffset
@@ -126,12 +196,25 @@ def test_timedelta_push_over_dst_boundary(self):
         # #1389
 
         # 4 hours before DST transition
-        stamp = Timestamp('3/10/2012 22:00', tz='US/Eastern')
+        stamp = Timestamp('3/10/2012 22:00', tz=self.tzstr('US/Eastern'))
 
         result = stamp + timedelta(hours=6)
 
         # spring forward, + "7" hours
-        expected = Timestamp('3/11/2012 05:00', tz='US/Eastern')
+        expected = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern'))
+
+        self.assertEquals(result, expected)
+
+    def test_timedelta_push_over_dst_boundary_explicit(self):
+        # #1389
+
+        # 4 hours before DST transition
+        stamp = Timestamp('3/10/2012 22:00', tz=self.tz('US/Eastern'))
+
+        result = stamp + timedelta(hours=6)
+
+        # spring forward, + "7" hours
+        expected = Timestamp('3/11/2012 05:00', tz=self.tz('US/Eastern'))
 
         self.assertEqual(result, expected)
 
@@ -140,7 +223,7 @@ def test_tz_localize_dti(self):
 
         dti = DatetimeIndex(start='1/1/2005', end='1/1/2005 0:00:30.256',
                             freq='L')
-        dti2 = dti.tz_localize('US/Eastern')
+        dti2 = dti.tz_localize(self.tzstr('US/Eastern'))
 
         dti_utc = DatetimeIndex(start='1/1/2005 05:00',
                                 end='1/1/2005 5:00:30.256', freq='L',
@@ -148,18 +231,18 @@ def test_tz_localize_dti(self):
 
         self.assert_numpy_array_equal(dti2.values, dti_utc.values)
 
-        dti3 = dti2.tz_convert('US/Pacific')
+        dti3 = dti2.tz_convert(self.tzstr('US/Pacific'))
         self.assert_numpy_array_equal(dti3.values, dti_utc.values)
 
         dti = DatetimeIndex(start='11/6/2011 1:59',
                             end='11/6/2011 2:00', freq='L')
         self.assertRaises(pytz.AmbiguousTimeError, dti.tz_localize,
-                          'US/Eastern')
+                          self.tzstr('US/Eastern'))
 
         dti = DatetimeIndex(start='3/13/2011 1:59', end='3/13/2011 2:00',
                             freq='L')
         self.assertRaises(
-            pytz.NonExistentTimeError, dti.tz_localize, 'US/Eastern')
+            pytz.NonExistentTimeError, dti.tz_localize, self.tzstr('US/Eastern'))
 
     def test_tz_localize_empty_series(self):
         # #2248
@@ -169,22 +252,22 @@ def test_tz_localize_empty_series(self):
         ts2 = ts.tz_localize('utc')
         self.assertTrue(ts2.index.tz == pytz.utc)
 
-        ts2 = ts.tz_localize('US/Eastern')
-        self.assertTrue(ts2.index.tz == pytz.timezone('US/Eastern'))
+        ts2 = ts.tz_localize(self.tzstr('US/Eastern'))
+        self.assertTrue(self.cmptz(ts2.index.tz, self.tz('US/Eastern')))
 
     def test_astimezone(self):
         utc = Timestamp('3/11/2012 22:00', tz='UTC')
-        expected = utc.tz_convert('US/Eastern')
-        result = utc.astimezone('US/Eastern')
+        expected = utc.tz_convert(self.tzstr('US/Eastern'))
+        result = utc.astimezone(self.tzstr('US/Eastern'))
         self.assertEqual(expected, result)
         tm.assert_isinstance(result, Timestamp)
 
     def test_create_with_tz(self):
-        stamp = Timestamp('3/11/2012 05:00', tz='US/Eastern')
+        stamp = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern'))
         self.assertEqual(stamp.hour, 5)
 
         rng = date_range(
-            '3/11/2012 04:00', periods=10, freq='H', tz='US/Eastern')
+            '3/11/2012 04:00', periods=10, freq='H', tz=self.tzstr('US/Eastern'))
 
         self.assertEqual(stamp, rng[1])
 
@@ -257,10 +340,10 @@ def test_date_range_localize(self):
 
     def test_utc_box_timestamp_and_localize(self):
         rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc')
-        rng_eastern = rng.tz_convert('US/Eastern')
+        rng_eastern = rng.tz_convert(self.tzstr('US/Eastern'))
 
-        tz = pytz.timezone('US/Eastern')
-        expected = tz.normalize(rng[-1])
+        tz = self.tz('US/Eastern')
+        expected = rng[-1].astimezone(tz)
 
         stamp = rng_eastern[-1]
         self.assertEqual(stamp, expected)
@@ -268,15 +351,17 @@ def test_utc_box_timestamp_and_localize(self):
 
         # right tzinfo
         rng = date_range('3/13/2012', '3/14/2012', freq='H', tz='utc')
-        rng_eastern = rng.tz_convert('US/Eastern')
-        self.assertIn('EDT', repr(rng_eastern[0].tzinfo))
+        rng_eastern = rng.tz_convert(self.tzstr('US/Eastern'))
+        # test not valid for dateutil timezones.
+        # self.assertIn('EDT', repr(rng_eastern[0].tzinfo))
+        self.assert_('EDT' in repr(rng_eastern[0].tzinfo) or 'tzfile' in repr(rng_eastern[0].tzinfo))
 
     def test_timestamp_tz_convert(self):
         strdates = ['1/1/2012', '3/1/2012', '4/1/2012']
-        idx = DatetimeIndex(strdates, tz='US/Eastern')
+        idx = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern'))
 
-        conv = idx[0].tz_convert('US/Pacific')
-        expected = idx.tz_convert('US/Pacific')[0]
+        conv = idx[0].tz_convert(self.tzstr('US/Pacific'))
+        expected = idx.tz_convert(self.tzstr('US/Pacific'))[0]
 
         self.assertEqual(conv, expected)
 
@@ -284,27 +369,27 @@ def test_pass_dates_localize_to_utc(self):
         strdates = ['1/1/2012', '3/1/2012', '4/1/2012']
 
         idx = DatetimeIndex(strdates)
-        conv = idx.tz_localize('US/Eastern')
+        conv = idx.tz_localize(self.tzstr('US/Eastern'))
 
-        fromdates = DatetimeIndex(strdates, tz='US/Eastern')
+        fromdates = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern'))
 
         self.assertEqual(conv.tz, fromdates.tz)
         self.assert_numpy_array_equal(conv.values, fromdates.values)
 
     def test_field_access_localize(self):
         strdates = ['1/1/2012', '3/1/2012', '4/1/2012']
-        rng = DatetimeIndex(strdates, tz='US/Eastern')
+        rng = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern'))
         self.assertTrue((rng.hour == 0).all())
 
         # a more unusual time zone, #1946
         dr = date_range('2011-10-02 00:00', freq='h', periods=10,
-                        tz='America/Atikokan')
+                        tz=self.tzstr('America/Atikokan'))
 
         expected = np.arange(10)
         self.assert_numpy_array_equal(dr.hour, expected)
 
     def test_with_tz(self):
-        tz = pytz.timezone('US/Central')
+        tz = self.tz('US/Central')
 
         # just want it to work
         start = datetime(2011, 3, 12, tzinfo=pytz.utc)
@@ -317,10 +402,11 @@ def test_with_tz(self):
 
         # normalized
         central = dr.tz_convert(tz)
+        self.assertIs(central.tz, tz)
+        self.assertIs(central[0].tz, tz)
 
         # compare vs a localized tz
-        comp = tz.localize(dr[0].to_pydatetime().replace(tzinfo=None)).tzinfo
-        self.assertIs(central.tz, tz)
+        comp = self.localize(tz, dr[0].to_pydatetime().replace(tzinfo=None)).tzinfo
         self.assertIs(central[0].tz, comp)
 
         # datetimes with tzinfo set
@@ -338,9 +424,7 @@ def test_tz_localize(self):
         self.assert_numpy_array_equal(dr_utc, localized)
 
     def test_with_tz_ambiguous_times(self):
-        tz = pytz.timezone('US/Eastern')
-
-        rng = bdate_range(datetime(2009, 1, 1), datetime(2010, 1, 1))
+        tz = self.tz('US/Eastern')
 
         # March 13, 2011, spring forward, skip from 2 AM to 3 AM
         dr = date_range(datetime(2011, 3, 13, 1, 30), periods=3,
@@ -363,7 +447,7 @@ def test_with_tz_ambiguous_times(self):
     def test_infer_dst(self):
         # November 6, 2011, fall back, repeat 2 AM hour
         # With no repeated hours, we cannot infer the transition
-        tz = pytz.timezone('US/Eastern')
+        tz = self.tz('US/Eastern')
         dr = date_range(datetime(2011, 11, 6, 0), periods=5,
                         freq=datetools.Hour())
         self.assertRaises(pytz.AmbiguousTimeError, dr.tz_localize,
@@ -388,36 +472,36 @@ def test_infer_dst(self):
 
     # test utility methods
     def test_infer_tz(self):
-        eastern = pytz.timezone('US/Eastern')
+        eastern = self.tz('US/Eastern')
         utc = pytz.utc
 
         _start = datetime(2001, 1, 1)
         _end = datetime(2009, 1, 1)
 
-        start = eastern.localize(_start)
-        end = eastern.localize(_end)
-        assert(tools._infer_tzinfo(start, end) is eastern.localize(_start).tzinfo)
-        assert(tools._infer_tzinfo(start, None) is eastern.localize(_start).tzinfo)
-        assert(tools._infer_tzinfo(None, end) is eastern.localize(_end).tzinfo)
+        start = self.localize(eastern, _start)
+        end = self.localize(eastern, _end)
+        assert(tools._infer_tzinfo(start, end) is self.localize(eastern, _start).tzinfo)
+        assert(tools._infer_tzinfo(start, None) is self.localize(eastern, _start).tzinfo)
+        assert(tools._infer_tzinfo(None, end) is self.localize(eastern, _end).tzinfo)
 
         start = utc.localize(_start)
         end = utc.localize(_end)
         assert(tools._infer_tzinfo(start, end) is utc)
 
-        end = eastern.localize(_end)
+        end = self.localize(eastern, _end)
         self.assertRaises(Exception, tools._infer_tzinfo, start, end)
         self.assertRaises(Exception, tools._infer_tzinfo, end, start)
 
     def test_tz_string(self):
-        result = date_range('1/1/2000', periods=10, tz='US/Eastern')
+        result = date_range('1/1/2000', periods=10, tz=self.tzstr('US/Eastern'))
         expected = date_range('1/1/2000', periods=10,
-                              tz=pytz.timezone('US/Eastern'))
+                              tz=self.tz('US/Eastern'))
 
         self.assertTrue(result.equals(expected))
 
     def test_take_dont_lose_meta(self):
         _skip_if_no_pytz()
-        rng = date_range('1/1/2000', periods=20, tz='US/Eastern')
+        rng = date_range('1/1/2000', periods=20, tz=self.tzstr('US/Eastern'))
 
         result = rng.take(lrange(5))
         self.assertEqual(result.tz, rng.tz)
@@ -426,7 +510,7 @@ def test_take_dont_lose_meta(self):
     def test_index_with_timezone_repr(self):
         rng = date_range('4/13/2010', '5/6/2010')
 
-        rng_eastern = rng.tz_localize('US/Eastern')
+        rng_eastern = rng.tz_localize(self.tzstr('US/Eastern'))
 
         rng_repr = repr(rng_eastern)
         self.assertIn('2010-04-13 00:00:00', rng_repr)
@@ -435,7 +519,7 @@ def test_index_astype_asobject_tzinfos(self):
         # #1345
 
         # dates around a dst transition
-        rng = date_range('2/13/2010', '5/6/2010', tz='US/Eastern')
+        rng = date_range('2/13/2010', '5/6/2010', tz=self.tzstr('US/Eastern'))
 
         objs = rng.asobject
         for i, x in enumerate(objs):
@@ -455,21 +539,21 @@ def test_localized_at_time_between_time(self):
         rng = date_range('4/16/2012', '5/1/2012', freq='H')
         ts = Series(np.random.randn(len(rng)), index=rng)
 
-        ts_local = ts.tz_localize('US/Eastern')
+        ts_local = ts.tz_localize(self.tzstr('US/Eastern'))
 
         result = ts_local.at_time(time(10, 0))
-        expected = ts.at_time(time(10, 0)).tz_localize('US/Eastern')
+        expected = ts.at_time(time(10, 0)).tz_localize(self.tzstr('US/Eastern'))
         assert_series_equal(result, expected)
-        self.assertEqual(result.index.tz.zone, 'US/Eastern')
+        self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern')))
 
         t1, t2 = time(10, 0), time(11, 0)
         result = ts_local.between_time(t1, t2)
-        expected = ts.between_time(t1, t2).tz_localize('US/Eastern')
+        expected = ts.between_time(t1, t2).tz_localize(self.tzstr('US/Eastern'))
         assert_series_equal(result, expected)
-        self.assertEqual(result.index.tz.zone, 'US/Eastern')
+        self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern')))
 
     def test_string_index_alias_tz_aware(self):
-        rng = date_range('1/1/2000', periods=10, tz='US/Eastern')
+        rng = date_range('1/1/2000', periods=10, tz=self.tzstr('US/Eastern'))
         ts = Series(np.random.randn(len(rng)), index=rng)
 
         result = ts['1/3/2000']
@@ -494,14 +578,14 @@ def test_fixedtz_topydatetime(self):
     def test_convert_tz_aware_datetime_datetime(self):
         # #1581
 
-        tz = pytz.timezone('US/Eastern')
+        tz = self.tz('US/Eastern')
 
         dates = [datetime(2000, 1, 1), datetime(2000, 1, 2),
                  datetime(2000, 1, 3)]
 
-        dates_aware = [tz.localize(x) for x in dates]
+        dates_aware = [self.localize(tz, x) for x in dates]
         result = to_datetime(dates_aware)
-        self.assertEqual(result.tz.zone, 'US/Eastern')
+        self.assertTrue(self.cmptz(result.tz, self.tz('US/Eastern')))
 
         converted = to_datetime(dates_aware, utc=True)
         ex_vals = [Timestamp(x).value for x in dates_aware]
@@ -534,7 +618,7 @@ def test_to_datetime_tzlocal(self):
     def test_frame_no_datetime64_dtype(self):
 
         dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI')
-        dr_tz = dr.tz_localize('US/Eastern')
+        dr_tz = dr.tz_localize(self.tzstr('US/Eastern'))
         e = DataFrame({'A': 'foo', 'B': dr_tz}, index=dr)
         self.assertEqual(e['B'].dtype, 'M8[ns]')
 
@@ -558,7 +642,7 @@ def test_hongkong_tz_convert(self):
 
     def test_tz_convert_unsorted(self):
         dr = date_range('2012-03-09', freq='H', periods=100, tz='utc')
-        dr = dr.tz_convert('US/Eastern')
+        dr = dr.tz_convert(self.tzstr('US/Eastern'))
 
         result = dr[::-1].hour
         exp = dr.hour[::-1]
@@ -566,14 +650,14 @@ def test_tz_convert_unsorted(self):
 
     def test_shift_localized(self):
         dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI')
-        dr_tz = dr.tz_localize('US/Eastern')
+        dr_tz = dr.tz_localize(self.tzstr('US/Eastern'))
 
         result = dr_tz.shift(1, '10T')
         self.assertEqual(result.tz, dr_tz.tz)
 
     def test_tz_aware_asfreq(self):
         dr = date_range(
-            '2011-12-01', '2012-07-20', freq='D', tz='US/Eastern')
+            '2011-12-01', '2012-07-20', freq='D', tz=self.tzstr('US/Eastern'))
 
         s = Series(np.random.randn(len(dr)), index=dr)
 
@@ -582,15 +666,15 @@ def test_tz_aware_asfreq(self):
 
     def test_static_tzinfo(self):
         # it works!
-        index = DatetimeIndex([datetime(2012, 1, 1)], tz='EST')
+        index = DatetimeIndex([datetime(2012, 1, 1)], tz=self.tzstr('EST'))
         index.hour
         index[0]
 
     def test_tzaware_datetime_to_index(self):
-        d = [datetime(2012, 8, 19, tzinfo=pytz.timezone('US/Eastern'))]
+        d = [datetime(2012, 8, 19, tzinfo=self.tz('US/Eastern'))]
 
         index = DatetimeIndex(d)
-        self.assertEqual(index.tz.zone, 'US/Eastern')
+        self.assertTrue(self.cmptz(index.tz, self.tz('US/Eastern')))
 
     def test_date_range_span_dst_transition(self):
         # #1778
@@ -601,11 +685,11 @@ def test_date_range_span_dst_transition(self):
 
         self.assertTrue((dr.hour == 0).all())
 
-        dr = date_range('2012-11-02', periods=10, tz='US/Eastern')
+        dr = date_range('2012-11-02', periods=10, tz=self.tzstr('US/Eastern'))
         self.assertTrue((dr.hour == 0).all())
 
     def test_convert_datetime_list(self):
-        dr = date_range('2012-06-02', periods=10, tz='US/Eastern')
+        dr = date_range('2012-06-02', periods=10, tz=self.tzstr('US/Eastern'))
 
         dr2 = DatetimeIndex(list(dr), name='foo')
         self.assertTrue(dr.equals(dr2))
@@ -620,7 +704,7 @@ def test_frame_from_records_utc(self):
         DataFrame.from_records([rec], index='begin_time')
 
     def test_frame_reset_index(self):
-        dr = date_range('2012-06-02', periods=10, tz='US/Eastern')
+        dr = date_range('2012-06-02', periods=10, tz=self.tzstr('US/Eastern'))
         df = DataFrame(np.random.randn(len(dr)), dr)
         roundtripped = df.reset_index().set_index('index')
         xp = df.index.tz
@@ -643,10 +727,10 @@ def test_dateutil_tzoffset_support(self):
     def test_getitem_pydatetime_tz(self):
         index = date_range(start='2012-12-24 16:00',
                            end='2012-12-24 18:00', freq='H',
-                           tz='Europe/Berlin')
+                           tz=self.tzstr('Europe/Berlin'))
         ts = Series(index=index, data=index.hour)
-        time_pandas = Timestamp('2012-12-24 17:00', tz='Europe/Berlin')
-        time_datetime = pytz.timezone('Europe/Berlin').localize(datetime(2012, 12, 24, 17, 0))
+        time_pandas = Timestamp('2012-12-24 17:00', tz=self.tzstr('Europe/Berlin'))
+        time_datetime = self.localize(self.tz('Europe/Berlin'), datetime(2012, 12, 24, 17, 0))
         self.assertEqual(ts[time_pandas], ts[time_datetime])
 
     def test_index_drop_dont_lose_tz(self):
@@ -663,21 +747,43 @@ def test_datetimeindex_tz(self):
 
         arr = ['11/10/2005 08:00:00', '11/10/2005 09:00:00']
 
-        idx1 = to_datetime(arr).tz_localize('US/Eastern')
-        idx2 = DatetimeIndex(start="2005-11-10 08:00:00", freq='H', periods=2, tz='US/Eastern')
-        idx3 = DatetimeIndex(arr, tz='US/Eastern')
-        idx4 = DatetimeIndex(np.array(arr), tz='US/Eastern')
+        idx1 = to_datetime(arr).tz_localize(self.tzstr('US/Eastern'))
+        idx2 = DatetimeIndex(start="2005-11-10 08:00:00", freq='H', periods=2, tz=self.tzstr('US/Eastern'))
+        idx3 = DatetimeIndex(arr, tz=self.tzstr('US/Eastern'))
+        idx4 = DatetimeIndex(np.array(arr), tz=self.tzstr('US/Eastern'))
 
         for other in [idx2, idx3, idx4]:
             self.assertTrue(idx1.equals(other))
 
     def test_datetimeindex_tz_nat(self):
-        idx = to_datetime([Timestamp("2013-1-1", tz='US/Eastern'), NaT])
+        idx = to_datetime([Timestamp("2013-1-1", tz=self.tzstr('US/Eastern')), NaT])
 
         self.assertTrue(isnull(idx[1]))
         self.assertTrue(idx[0].tzinfo is not None)
 
 
+class TestTimeZoneSupportDateutil(TestTimeZoneSupportPytz):
+    _multiprocess_can_split_ = True
+
+    def setUp(self):
+        _skip_if_no_dateutil()
+
+    def tz(self, tz):
+        ''' Construct a timezone object from a string. Overridden in subclass to parameterize tests. '''
+        return dateutil.tz.gettz(tz)
+
+    def tzstr(self, tz):
+        ''' Construct a timezone string from a string. Overridden in subclass to parameterize tests. '''
+        return 'dateutil/' + tz
+
+    def cmptz(self, tz1, tz2):
+        ''' Compare two timezones. Overridden in subclass to parameterize tests. '''
+        return tz1 == tz2
+
+    def localize(self, tz, x):
+        return x.replace(tzinfo=tz)
+
+
 class TestTimeZones(tm.TestCase):
     _multiprocess_can_split_ = True