diff --git a/jenkins/__init__.py b/jenkins/__init__.py
index 4efe64b..3029472 100644
--- a/jenkins/__init__.py
+++ b/jenkins/__init__.py
@@ -46,10 +46,13 @@ See examples at :doc:`example`
 '''
 
 import base64
-from httplib import BadStatusLine
 import json
-import urllib
-from urllib2 import Request, HTTPError, urlopen
+
+import six
+from six.moves.http_client import BadStatusLine
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.parse import quote, urlencode
+from six.moves.urllib.request import Request, urlopen
 
 LAUNCHER_SSH = 'hudson.plugins.sshslaves.SSHLauncher'
 LAUNCHER_COMMAND = 'hudson.slaves.CommandLauncher'
@@ -129,7 +132,10 @@ def auth_headers(username, password):
     Simple implementation of HTTP Basic Authentication. Returns the
     'Authentication' header value.
     '''
-    return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
+    auth = '%s:%s' % (username, password)
+    if isinstance(auth, six.text_type):
+        auth = auth.encode('utf-8')
+    return b'Basic ' + base64.encodestring(auth)[:-1]
 
 
 class Jenkins(object):
@@ -160,7 +166,7 @@ class Jenkins(object):
             response = self.jenkins_open(Request(
                 self.server + CRUMB_URL), add_crumb=False)
             if response:
-                self.crumb = json.loads(response)
+                self.crumb = json.loads(response.decode('utf-8'))
             else:
                 # Don't need crumbs
                 self.crumb = False
@@ -212,8 +218,8 @@ class Jenkins(object):
         '''
         Print out job info in more readable format
         '''
-        for k, v in self.get_job_info(job_name).iteritems():
-            print k, v
+        for k, v in self.get_job_info(job_name).items():
+            print(k, v)
 
     def jenkins_open(self, req, add_crumb=True):
         '''
@@ -227,7 +233,7 @@ class Jenkins(object):
             if add_crumb:
                 self.maybe_add_crumb(req)
             return urlopen(req).read()
-        except HTTPError, e:
+        except HTTPError as e:
             # Jenkins's funky authentication means its nigh impossible to
             # distinguish errors.
             if e.code in [401, 403, 500]:
@@ -425,7 +431,7 @@ class Jenkins(object):
         :returns: job configuration (XML format)
         '''
         request = Request(self.server + CONFIG_JOB %
-                          {"name": urllib.quote(name)})
+                          {"name": quote(name)})
         return self.jenkins_open(request)
 
     def reconfig_job(self, name, config_xml):
@@ -454,10 +460,10 @@ class Jenkins(object):
             if token:
                 parameters['token'] = token
             return (self.server + BUILD_WITH_PARAMS_JOB % locals() +
-                    '?' + urllib.urlencode(parameters))
+                    '?' + urlencode(parameters))
         elif token:
             return (self.server + BUILD_JOB % locals() +
-                    '?' + urllib.urlencode({'token': token}))
+                    '?' + urlencode({'token': token}))
         else:
             return self.server + BUILD_JOB % locals()
 
@@ -597,7 +603,7 @@ class Jenkins(object):
         }
 
         self.jenkins_open(Request(
-            self.server + CREATE_NODE % urllib.urlencode(params)))
+            self.server + CREATE_NODE % urlencode(params)))
 
         if not self.node_exists(name):
             raise JenkinsException('create[%s] failed' % (name))
diff --git a/test-requirements.txt b/test-requirements.txt
index 69825cc..3cbc320 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,4 +2,4 @@ coverage>=3.6
 discover
 flake8
 mock
-unittest2
+six
diff --git a/tests/helper.py b/tests/helper.py
index 4962a61..a712654 100644
--- a/tests/helper.py
+++ b/tests/helper.py
@@ -3,4 +3,3 @@ import sys
 sys.path.insert(0, os.path.abspath('..'))
 
 import jenkins  # noqa
-from StringIO import StringIO  # noqa
diff --git a/tests/test_jenkins.py b/tests/test_jenkins.py
index 3bba110..8206372 100644
--- a/tests/test_jenkins.py
+++ b/tests/test_jenkins.py
@@ -1,9 +1,20 @@
 import json
-import unittest2 as unittest
+import sys
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
 
 from mock import patch
+import six
 
-from tests.helper import jenkins, StringIO
+from tests.helper import jenkins
+
+
+def get_mock_urlopen_return_value(a_dict=None):
+    if a_dict is None:
+        a_dict = {}
+    return six.BytesIO(json.dumps(a_dict).encode('utf-8'))
 
 
 class JenkinsTest(unittest.TestCase):
@@ -11,13 +22,13 @@ class JenkinsTest(unittest.TestCase):
     def test_constructor_url_with_trailing_slash(self):
         j = jenkins.Jenkins('http://example.com/', 'test', 'test')
         self.assertEqual(j.server, 'http://example.com/')
-        self.assertEqual(j.auth, 'Basic dGVzdDp0ZXN0')
+        self.assertEqual(j.auth, b'Basic dGVzdDp0ZXN0')
         self.assertEqual(j.crumb, None)
 
     def test_constructor_url_without_trailing_slash(self):
         j = jenkins.Jenkins('http://example.com', 'test', 'test')
         self.assertEqual(j.server, 'http://example.com/')
-        self.assertEqual(j.auth, 'Basic dGVzdDp0ZXN0')
+        self.assertEqual(j.auth, b'Basic dGVzdDp0ZXN0')
         self.assertEqual(j.crumb, None)
 
     def test_constructor_without_user_or_password(self):
@@ -26,6 +37,14 @@ class JenkinsTest(unittest.TestCase):
         self.assertEqual(j.auth, None)
         self.assertEqual(j.crumb, None)
 
+    def test_constructor_unicode_password(self):
+        j = jenkins.Jenkins('http://example.com',
+                            six.u('nonascii'),
+                            six.u('\xe9\u20ac'))
+        self.assertEqual(j.server, 'http://example.com/')
+        self.assertEqual(j.auth, b'Basic bm9uYXNjaWk6w6nigqw=')
+        self.assertEqual(j.crumb, None)
+
     @patch.object(jenkins.Jenkins, 'jenkins_open')
     def test_get_job_config_encodes_job_name(self, jenkins_mock):
         """
@@ -40,7 +59,7 @@ class JenkinsTest(unittest.TestCase):
 
     @patch('jenkins.urlopen')
     def test_maybe_add_crumb(self, jenkins_mock):
-        jenkins_mock.return_value = StringIO()
+        jenkins_mock.return_value = get_mock_urlopen_return_value()
         j = jenkins.Jenkins('http://example.com/', 'test', 'test')
         request = jenkins.Request('http://example.com/job/TestJob')
 
@@ -58,7 +77,7 @@ class JenkinsTest(unittest.TestCase):
             "crumb": "dab177f483b3dd93483ef6716d8e792d",
             "crumbRequestField": ".crumb",
         }
-        jenkins_mock.return_value = StringIO(json.dumps(crumb_data))
+        jenkins_mock.return_value = get_mock_urlopen_return_value(crumb_data)
         j = jenkins.Jenkins('http://example.com/', 'test', 'test')
         request = jenkins.Request('http://example.com/job/TestJob')
 
@@ -78,8 +97,8 @@ class JenkinsTest(unittest.TestCase):
         }
         data = {'foo': 'bar'}
         jenkins_mock.side_effect = [
-            StringIO(json.dumps(crumb_data)),
-            StringIO(json.dumps(data)),
+            get_mock_urlopen_return_value(crumb_data),
+            get_mock_urlopen_return_value(data),
         ]
         j = jenkins.Jenkins('http://example.com/', 'test', 'test')
         request = jenkins.Request('http://example.com/job/TestJob')
@@ -89,7 +108,7 @@ class JenkinsTest(unittest.TestCase):
         self.assertEqual(
             jenkins_mock.call_args[0][0].get_full_url(),
             'http://example.com/job/TestJob')
-        self.assertEqual(response, json.dumps(data))
+        self.assertEqual(response, json.dumps(data).encode('utf-8'))
         self.assertEqual(j.crumb, crumb_data)
         self.assertEqual(request.headers['.crumb'], crumb_data['crumb'])
 
diff --git a/tox.ini b/tox.ini
index d9424b8..a5f8ced 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 minversion = 1.6
 skipsdist = True
-envlist = pep8, py26, py27
+envlist = pep8, py26, py27, pypy, py33, py34
 
 [testenv]
 setenv VIRTUAL_ENV={envdir}
@@ -15,6 +15,10 @@ commands =
     coverage run -m discover
     coverage report --show-missing
 
+[testenv:py26]
+deps = -r{toxinidir}/test-requirements.txt
+       unittest2
+
 [tox:jenkins]
 downloadcache = ~/cache/pip