diff --git a/.travis.yml b/.travis.yml
index 1505fccb9e5bd689f6964ac911b32fa86dac2b92..5d8f2f0fef02feacca7bf20e861675293df3dd2f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
 sudo: false
 language: python
 python:
-  - "2.7"
+  - "3.7"
 
 os: linux
 
@@ -9,13 +9,13 @@ before_install:
   - export BOTO_CONFIG=/dev/null
 
 install:
-  - pip install --upgrade pip
-  - pip install -r requirements.txt
+  - pip3 install --upgrade pip
+  - pip3 install -r requirements.txt
 
 before_script:
   - psql -c 'create database helios;' -U postgres
 
-script: "python -Wall manage.py test"
+script: "python3 -Wall manage.py test"
 
 jobs:
   include:
diff --git a/helios/__init__.py b/helios/__init__.py
index 2f598298b565f3a883cf9365699a6911031e0120..21f21bc59da930240e0c88612f1afcc9c0a46178 100644
--- a/helios/__init__.py
+++ b/helios/__init__.py
@@ -1,7 +1,7 @@
 from django.conf import settings
 # This will make sure the app is always imported when
 # Django starts so that shared_task will use this app.
-from celery_app import app as celery_app
+from .celery_app import app as celery_app
 
 __all__ = ('celery_app', 'TEMPLATE_BASE', 'ADMIN_ONLY', 'VOTERS_UPLOAD', 'VOTERS_EMAIL',)
 
diff --git a/helios/crypto/algs.py b/helios/crypto/algs.py
index 8ef36212bef0e8dfd87963585e5d3d7845637415..b7439d9bd3d50cd738b354fcfbfe64495df0db78 100644
--- a/helios/crypto/algs.py
+++ b/helios/crypto/algs.py
@@ -6,14 +6,14 @@ FIXME: improve random number generation.
 Ben Adida
 ben@adida.net
 """
-from __future__ import print_function
 
-import hashlib
 import logging
 
+from Crypto.Hash import SHA1
 from Crypto.Util import number
 
 from helios.crypto.utils import random
+from helios.utils import to_json
 
 
 class ElGamal:
@@ -117,8 +117,7 @@ class EGPublicKey:
 
     # quick hack FIXME
     def toJSON(self):
-        import utils
-        return utils.to_json(self.toJSONDict())
+        return to_json(self.toJSONDict())
 
     def __mul__(self, other):
         if other == 0 or other == 1:
@@ -263,7 +262,7 @@ class EGSecretKey:
         a = pow(self.pk.g, w, self.pk.p)
         b = pow(ciphertext.alpha, w, self.pk.p)
 
-        c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(), 16)
+        c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(), 16)
 
         t = (w + self.x * c) % self.pk.q
 
@@ -299,7 +298,7 @@ class EGSecretKey:
 
         sk = cls()
         sk.x = int(d['x'])
-        if d.has_key('public_key'):
+        if 'public_key' in d:
             sk.pk = EGPublicKey.from_dict(d['public_key'])
         else:
             sk.pk = None
@@ -333,7 +332,7 @@ class EGCiphertext:
         """
         Homomorphic Multiplication of ciphertexts.
         """
-        if type(other) == int and (other == 0 or other == 1):
+        if isinstance(other, int) and (other == 0 or other == 1):
             return self
 
         if self.pk != other.pk:
@@ -699,7 +698,7 @@ def EG_disjunctive_challenge_generator(commitments):
         array_to_hash.append(str(commitment['B']))
 
     string_to_hash = ",".join(array_to_hash)
-    return int(hashlib.sha1(string_to_hash).hexdigest(), 16)
+    return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
 
 
 # a challenge generator for Fiat-Shamir with A,B commitment
@@ -709,4 +708,4 @@ def EG_fiatshamir_challenge_generator(commitment):
 
 def DLog_challenge_generator(commitment):
     string_to_hash = str(commitment)
-    return int(hashlib.sha1(string_to_hash).hexdigest(), 16)
+    return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16)
diff --git a/helios/crypto/electionalgs.py b/helios/crypto/electionalgs.py
index b9a52753e75fa78ddbda47dfa53c83540bbc1fe1..1f7593b14031ffb397352dcdf52a721e984f52d6 100644
--- a/helios/crypto/electionalgs.py
+++ b/helios/crypto/electionalgs.py
@@ -7,8 +7,9 @@ Ben Adida
 import datetime
 import uuid
 
-import algs
-import utils
+from helios.utils import to_json
+from . import algs
+from . import utils
 
 
 class HeliosObject(object):
@@ -29,7 +30,7 @@ class HeliosObject(object):
 
     def set_from_args(self, **kwargs):
         for f in self.FIELDS:
-            if kwargs.has_key(f):
+            if f in kwargs:
                 new_val = self.process_value_in(f, kwargs[f])
                 setattr(self, f, new_val)
             else:
@@ -43,7 +44,7 @@ class HeliosObject(object):
                 setattr(self, f, None)
 
     def toJSON(self):
-        return utils.to_json(self.toJSONDict())
+        return to_json(self.toJSONDict())
 
     def toJSONDict(self, alternate_fields=None):
         val = {}
@@ -55,7 +56,7 @@ class HeliosObject(object):
     def fromJSONDict(cls, d):
         # go through the keys and fix them
         new_d = {}
-        for k in d.keys():
+        for k in list(d.keys()):
             new_d[str(k)] = d[k]
 
         return cls(**new_d)
@@ -78,7 +79,7 @@ class HeliosObject(object):
 
     @property
     def hash(self):
-        s = utils.to_json(self.toJSONDict())
+        s = to_json(self.toJSONDict())
         return utils.hash_b64(s)
 
     def process_value_in(self, field_name, field_value):
@@ -230,7 +231,7 @@ class EncryptedAnswer(HeliosObject):
         else:
             ea.overall_proof = None
 
-        if d.has_key('randomness'):
+        if 'randomness' in d:
             ea.randomness = [int(r) for r in d['randomness']]
             ea.answer = d['answer']
 
@@ -264,7 +265,7 @@ class EncryptedAnswer(HeliosObject):
 
         # min and max for number of answers, useful later
         min_answers = 0
-        if question.has_key('min'):
+        if 'min' in question:
             min_answers = question['min']
         max_answers = question['max']
 
@@ -340,7 +341,7 @@ class EncryptedVote(HeliosObject):
 
             question = election.questions[question_num]
             min_answers = 0
-            if question.has_key('min'):
+            if 'min' in question:
                 min_answers = question['min']
 
             if not ea.verify(election.public_key, min=min_answers, max=question['max']):
@@ -349,7 +350,7 @@ class EncryptedVote(HeliosObject):
         return True
 
     def get_hash(self):
-        return utils.hash_b64(utils.to_json(self.toJSONDict()))
+        return utils.hash_b64(to_json(self.toJSONDict()))
 
     def toJSONDict(self, with_randomness=False):
         return {
@@ -383,7 +384,7 @@ def one_question_winner(question, result, num_cast_votes):
     determining the winner for one question
     """
     # sort the answers , keep track of the index
-    counts = sorted(enumerate(result), key=lambda (x): x[1])
+    counts = sorted(enumerate(result), key=lambda x: x[1])
     counts.reverse()
 
     # if there's a max > 1, we assume that the top MAX win
@@ -416,7 +417,7 @@ class Election(HeliosObject):
 
     def _process_value_in(self, field_name, field_value):
         if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
-            if type(field_value) == str or type(field_value) == unicode:
+            if isinstance(field_value, str):
                 return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
 
         if field_name == 'public_key':
@@ -553,7 +554,7 @@ class CastVote(HeliosObject):
 
     def _process_value_in(self, field_name, field_value):
         if field_name == 'cast_at':
-            if type(field_value) == str:
+            if isinstance(field_value, str):
                 return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
 
         if field_name == 'vote':
diff --git a/helios/crypto/elgamal.py b/helios/crypto/elgamal.py
index c6b89b3820284e5c9df17849be4c5fe77e02d917..33eb03083319aa0b7f9ab61defbca3a50bd57440 100644
--- a/helios/crypto/elgamal.py
+++ b/helios/crypto/elgamal.py
@@ -192,7 +192,7 @@ class SecretKey:
         a = pow(self.pk.g, w, self.pk.p)
         b = pow(ciphertext.alpha, w, self.pk.p)
 
-        c = int(SHA1.new(str(a) + "," + str(b)).hexdigest(),16)
+        c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(),16)
 
         t = (w + self.x * c) % self.pk.q
 
@@ -232,7 +232,7 @@ class Ciphertext:
         """
         Homomorphic Multiplication of ciphertexts.
         """
-        if type(other) == int and (other == 0 or other == 1):
+        if isinstance(other, int) and (other == 0 or other == 1):
           return self
           
         if self.pk != other.pk:
@@ -278,10 +278,10 @@ class Ciphertext:
       """
       Check for ciphertext equality.
       """
-      if other == None:
+      if other is None:
         return False
         
-      return (self.alpha == other.alpha and self.beta == other.beta)
+      return self.alpha == other.alpha and self.beta == other.beta
     
     def generate_encryption_proof(self, plaintext, randomness, challenge_generator):
       """
@@ -393,7 +393,7 @@ class Ciphertext:
       for i in range(len(plaintexts)):
         # if a proof fails, stop right there
         if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]):
-          print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])
+          print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]))
           return False
           
       # logging.info("made it past the two encryption proofs")
@@ -503,7 +503,7 @@ def disjunctive_challenge_generator(commitments):
     array_to_hash.append(str(commitment['B']))
 
   string_to_hash = ",".join(array_to_hash)
-  return int(SHA1.new(string_to_hash).hexdigest(),16)
+  return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
   
 # a challenge generator for Fiat-Shamir with A,B commitment
 def fiatshamir_challenge_generator(commitment):
@@ -511,5 +511,5 @@ def fiatshamir_challenge_generator(commitment):
 
 def DLog_challenge_generator(commitment):
   string_to_hash = str(commitment)
-  return int(SHA1.new(string_to_hash).hexdigest(),16)
+  return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16)
 
diff --git a/helios/crypto/numtheory.py b/helios/crypto/numtheory.py
index 16fcf9aa0c2fe0017471546d0f353c9cb60878bd..e16691745cf7f9dd2713274b67c1a1a0091df12e 100644
--- a/helios/crypto/numtheory.py
+++ b/helios/crypto/numtheory.py
@@ -103,7 +103,7 @@ def trial_division(n, bound=None):
     if n == 1: return 1
     for p in [2, 3, 5]:
         if n%p == 0: return p
-    if bound == None: bound = n
+    if bound is None: bound = n
     dif = [6, 4, 2, 4, 2, 4, 6, 2]
     m = 7; i = 1
     while m <= bound and m*m <= n:
@@ -207,7 +207,7 @@ def inversemod(a, n):
     """
     g, x, y = xgcd(a, n)
     if g != 1:
-        raise ZeroDivisionError, (a,n)
+        raise ZeroDivisionError(a,n)
     assert g == 1, "a must be coprime to n."
     return x%n
 
@@ -225,7 +225,7 @@ def solve_linear(a,b,n):
     Examples:
     >>> solve_linear(4, 2, 10)
     8
-    >>> solve_linear(2, 1, 4) == None
+    >>> solve_linear(2, 1, 4) is None
     True
     """
     g, c, _ = xgcd(a,n)                 # (1)
@@ -1014,7 +1014,7 @@ def elliptic_curve_method(N, m, tries=5):
         E, P = randcurve(N)                    # (2)
         try:                                   # (3)
             Q = ellcurve_mul(E, m, P)          # (4)
-        except ZeroDivisionError, x:           # (5)
+        except ZeroDivisionError as x:         # (5)
             g = gcd(x[0],N)                    # (6)
             if g != 1 or g != N: return g      # (7)
     return N             
@@ -1153,7 +1153,7 @@ class Poly:                                     # (1)
         return r
     def __neg__(self):
         v = {}
-        for m in self.v.keys():
+        for m in list(self.v.keys()):
             v[m] = -self.v[m]
         return Poly(v)
     def __div__(self, other):
@@ -1161,7 +1161,7 @@ class Poly:                                     # (1)
 
     def __getitem__(self, m):                   # (6)
         m = tuple(m)
-        if not self.v.has_key(m): self.v[m] = 0
+        if m not in self.v: self.v[m] = 0
         return self.v[m]
     def __setitem__(self, m, c):
         self.v[tuple(m)] = c
@@ -1169,7 +1169,7 @@ class Poly:                                     # (1)
         del self.v[tuple(m)]
 
     def monomials(self):                        # (7)
-        return self.v.keys()
+        return list(self.v.keys())
     def normalize(self):                        # (8)
         while True:
             finished = True
@@ -1244,8 +1244,8 @@ def prove_associative():                        # (15)
                    - (x3 + x4)*(x3 - x4)*(x3 - x4))
     s2 = (x3 - x4)*(x3 - x4)*((y1 - y5)*(y1 - y5) \
                    - (x1 + x5)*(x1 - x5)*(x1 - x5))
-    print "Associative?"
-    print s1 == s2                              # (17)
+    print("Associative?")
+    print(s1 == s2)                              # (17)
 
 
 
diff --git a/helios/crypto/utils.py b/helios/crypto/utils.py
index 08df21b2ced75c8305dd223476c2a8bbb5c1f0eb..d854c886bd6e6a0128fbdeec232305ee97870a2a 100644
--- a/helios/crypto/utils.py
+++ b/helios/crypto/utils.py
@@ -2,7 +2,6 @@
 Crypto Utils
 """
 import base64
-import json
 import math
 
 from Crypto.Hash import SHA256
@@ -30,12 +29,3 @@ def hash_b64(s):
     hasher = SHA256.new(s.encode('utf-8'))
     result = base64.b64encode(hasher.digest())[:-1]
     return result
-
-
-def to_json(d):
-    return json.dumps(d, sort_keys=True)
-
-
-def from_json(json_str):
-    if not json_str: return None
-    return json.loads(json_str)
diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py
index 93bca2442f0dbe792bf879afd3e3c704c617a28d..574f8eb9355bcec57a4a60e40f9f8c2154a2af25 100644
--- a/helios/datatypes/__init__.py
+++ b/helios/datatypes/__init__.py
@@ -25,6 +25,7 @@ And when data comes in:
   # but is not necessary for full JSON-LD objects.
   LDObject.deserialize(json_string, type=...)
 """
+import importlib
 
 from helios import utils
 from helios.crypto import utils as cryptoutils
@@ -36,21 +37,21 @@ def recursiveToDict(obj):
     if obj is None:
         return None
 
-    if type(obj) == list:
+    if isinstance(obj, list):
         return [recursiveToDict(el) for el in obj]
     else:
         return obj.toDict()
 
 def get_class(datatype):
     # already done?
-    if not isinstance(datatype, basestring):
+    if not isinstance(datatype, str):
         return datatype
 
     # parse datatype string "v31/Election" --> from v31 import Election
     parsed_datatype = datatype.split("/")
     
     # get the module
-    dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1)
+    dynamic_module = importlib.import_module("helios.datatypes." + (".".join(parsed_datatype[:-1])))
     
     if not dynamic_module:
         raise Exception("no module for %s" % datatype)
@@ -58,7 +59,7 @@ def get_class(datatype):
     # go down the attributes to get to the class
     try:
         dynamic_ptr = dynamic_module
-        for attr in parsed_datatype[1:]:
+        for attr in parsed_datatype[-1:]:
             dynamic_ptr = getattr(dynamic_ptr, attr)
         dynamic_cls = dynamic_ptr
     except AttributeError:
@@ -119,7 +120,7 @@ class LDObject(object):
 
     @classmethod
     def instantiate(cls, obj, datatype=None):
-        "FIXME: should datatype override the object's internal datatype? probably not"
+        """FIXME: should datatype override the object's internal datatype? probably not"""
         if isinstance(obj, LDObject):
             return obj
 
@@ -149,9 +150,11 @@ class LDObject(object):
         setattr(self.wrapped_obj, attr, val)
 
     def loadData(self):
-        "load data using from the wrapped object"
+        """
+        load data using from the wrapped object
+        """
         # go through the subfields and instantiate them too
-        for subfield_name, subfield_type in self.STRUCTURED_FIELDS.iteritems():
+        for subfield_name, subfield_type in self.STRUCTURED_FIELDS.items():
             self.structured_fields[subfield_name] = self.instantiate(self._getattr_wrapped(subfield_name), datatype = subfield_type)
         
     def loadDataFromDict(self, d):
@@ -160,7 +163,7 @@ class LDObject(object):
         """
 
         # the structured fields
-        structured_fields = self.STRUCTURED_FIELDS.keys()
+        structured_fields = list(self.STRUCTURED_FIELDS.keys())
 
         # go through the fields and set them properly
         # on the newly instantiated object
@@ -195,7 +198,7 @@ class LDObject(object):
 
         for f in (alternate_fields or fields):
             # is it a structured subfield?
-            if self.structured_fields.has_key(f):
+            if f in self.structured_fields:
                 val[f] = recursiveToDict(self.structured_fields[f])
             else:
                 val[f] = self.process_value_out(f, self._getattr_wrapped(f))
@@ -274,8 +277,10 @@ class LDObject(object):
             return field_value
   
     def _process_value_out(self, field_name, field_value):
+        if isinstance(field_value, bytes):
+            return field_value.decode('utf-8')
         return None
-    
+
     def __eq__(self, other):
         if not hasattr(self, 'uuid'):
             return super(LDObject, self) == other
diff --git a/helios/datatypes/djangofield.py b/helios/datatypes/djangofield.py
index 05f8cf355845ddf0eed23d4efc93526a4a06e269..a299ace6c6a7b8f55e231a5e645d238aceb7f51c 100644
--- a/helios/datatypes/djangofield.py
+++ b/helios/datatypes/djangofield.py
@@ -6,9 +6,9 @@ http://www.djangosnippets.org/snippets/377/
 and adapted to LDObject
 """
 
-import json
 from django.db import models
 
+from helios import utils
 from . import LDObject
 
 
@@ -28,36 +28,26 @@ class LDObjectField(models.TextField):
         """Convert our string value to LDObject after we load it from the DB"""
 
         # did we already convert this?
-        if not isinstance(value, basestring):
+        if not isinstance(value, str):
             return value
 
         return self.from_db_value(value)
 
     # noinspection PyUnusedLocal
     def from_db_value(self, value, *args, **kwargs):
-        if value is None:
-            return None
-
         # in some cases, we're loading an existing array or dict,
-        # we skip this part but instantiate the LD object
-        if isinstance(value, basestring):
-            try:
-                parsed_value = json.loads(value)
-            except:
-                raise Exception("value is not JSON parseable, that's bad news")
-        else:
-            parsed_value = value
-
-        if parsed_value is not None:
-            # we give the wrapped object back because we're not dealing with serialization types
-            return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj
-            return return_val
-        else:
+        # from_json takes care of this duality
+        parsed_value = utils.from_json(value)
+        if parsed_value is None:
             return None
 
+        # we give the wrapped object back because we're not dealing with serialization types
+        return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj
+        return return_val
+
     def get_prep_value(self, value):
         """Convert our JSON object to a string before we save"""
-        if isinstance(value, basestring):
+        if isinstance(value, str):
             return value
 
         if value is None:
diff --git a/helios/datetimewidget.py b/helios/datetimewidget.py
index 81d81e0172f7538df77a8d8be8f95774c8bd4e99..dfd7ec04a42118460a216988fa2a9f5a323c1093 100644
--- a/helios/datetimewidget.py
+++ b/helios/datetimewidget.py
@@ -14,7 +14,7 @@ import datetime, time
 from django.utils.safestring import mark_safe
 
 # DATETIMEWIDGET
-calbtn = u'''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" />
+calbtn = '''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" />
 <script type="text/javascript">
     Calendar.setup({
         inputField     :    "%s",
@@ -51,13 +51,13 @@ class DateTimeWidget(forms.widgets.TextInput):
             except:
                 final_attrs['value'] = \
                                    force_unicode(value)
-        if not final_attrs.has_key('id'):
-            final_attrs['id'] = u'%s_id' % (name)
+        if 'id' not in final_attrs:
+            final_attrs['id'] = '%s_id' % (name)
         id = final_attrs['id']
 
         jsdformat = self.dformat #.replace('%', '%%')
         cal = calbtn % (settings.MEDIA_URL, id, id, jsdformat, id)
-        a = u'<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
+        a = '<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal)
         return mark_safe(a)
 
     def value_from_datadict(self, data, files, name):
@@ -84,12 +84,12 @@ class DateTimeWidget(forms.widgets.TextInput):
         Copy of parent's method, but modify value with strftime function before final comparsion
         """
         if data is None:
-            data_value = u''
+            data_value = ''
         else:
             data_value = data
 
         if initial is None:
-            initial_value = u''
+            initial_value = ''
         else:
             initial_value = initial
 
diff --git a/helios/fields.py b/helios/fields.py
index cf2ad6c3e7c2c6b506c0080b432f6207f4e37807..8d8e885fec3c13f72a0c24cd6ea0d7a39ab80d5c 100644
--- a/helios/fields.py
+++ b/helios/fields.py
@@ -1,9 +1,9 @@
-from time import strptime, strftime
 import datetime
-from django import forms
-from django.db import models
+
 from django.forms import fields
-from widgets import SplitSelectDateTimeWidget
+
+from .widgets import SplitSelectDateTimeWidget
+
 
 class SplitDateTimeField(fields.MultiValueField):
     widget = SplitSelectDateTimeWidget
diff --git a/helios/forms.py b/helios/forms.py
index 86fc18e5e3a0e88e4a65f32b62ef12b2bc1dea56..d7919675863641cd24abd5ba3082b0e6014d4e84 100644
--- a/helios/forms.py
+++ b/helios/forms.py
@@ -3,11 +3,12 @@ Forms for Helios
 """
 
 from django import forms
-from models import Election
-from widgets import SplitSelectDateTimeWidget
-from fields import SplitDateTimeField
 from django.conf import settings
 
+from .fields import SplitDateTimeField
+from .models import Election
+from .widgets import SplitSelectDateTimeWidget
+
 
 class ElectionForm(forms.Form):
   short_name = forms.SlugField(max_length=40, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010')
diff --git a/helios/management/commands/load_voter_files.py b/helios/management/commands/load_voter_files.py
index c34e682aba306f9bd1380a5ed1dafa91cb247b8b..1e3a79beeecf537b24963a5f2b80f1b46f653dfe 100644
--- a/helios/management/commands/load_voter_files.py
+++ b/helios/management/commands/load_voter_files.py
@@ -28,7 +28,7 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
                             dialect=dialect, **kwargs)
     for row in csv_reader:
         # decode UTF-8 back to Unicode, cell by cell:
-        yield [unicode(cell, 'utf-8') for cell in row]
+        yield [str(cell, 'utf-8') for cell in row]
 
 
 def utf_8_encoder(unicode_csv_data):
diff --git a/helios/migrations/0001_initial.py b/helios/migrations/0001_initial.py
index 8ba6efbb125a7b1c4e1e3ed6e74b831bedaf25f1..886b370219ab7fd2fc6fb218148761ae5c4d026c 100644
--- a/helios/migrations/0001_initial.py
+++ b/helios/migrations/0001_initial.py
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
+
+import helios.datatypes
 import helios.datatypes.djangofield
 import helios_auth.jsonfield
-import helios.datatypes
 
 
 class Migration(migrations.Migration):
diff --git a/helios/migrations/0002_castvote_cast_ip.py b/helios/migrations/0002_castvote_cast_ip.py
index 47db5b169eccee318b2ccb4fc18f125e984a9c83..bb7a422639f66cd81974549a95c000204ebe13da 100644
--- a/helios/migrations/0002_castvote_cast_ip.py
+++ b/helios/migrations/0002_castvote_cast_ip.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
 
diff --git a/helios/migrations/0003_auto_20160507_1948.py b/helios/migrations/0003_auto_20160507_1948.py
index 162d6bb54dcb6c1e74c99e859615d7fe51dc179d..8e6f65266e5c6f04bf3cb07b8927ac94ea2ece95 100644
--- a/helios/migrations/0003_auto_20160507_1948.py
+++ b/helios/migrations/0003_auto_20160507_1948.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
 
diff --git a/helios/migrations/0004_auto_20170528_2025.py b/helios/migrations/0004_auto_20170528_2025.py
index d49bb63753f4438ab79c0ce4101f7e56f7bcf8be..a437c5f11f03a5a0766ecd970a031bda409fe3fd 100644
--- a/helios/migrations/0004_auto_20170528_2025.py
+++ b/helios/migrations/0004_auto_20170528_2025.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import migrations, models
 
diff --git a/helios/models.py b/helios/models.py
index 6cb505524e812dfc2da8b3ff0b73d04d6a834dfa..29596f5077f244f91df6a97e1248a095cfe2feb8 100644
--- a/helios/models.py
+++ b/helios/models.py
@@ -6,26 +6,26 @@ Ben Adida
 (ben@adida.net)
 """
 
-import datetime
-
-import bleach
 import copy
 import csv
+import datetime
 import io
-import unicodecsv
 import uuid
+
+import bleach
+import unicodecsv
 from django.conf import settings
 from django.db import models, transaction
 
-from crypto import algs, utils
 from helios import datatypes
-from helios import utils as heliosutils
-from helios.crypto.elgamal import Cryptosystem
-from helios.crypto.utils import random
+from helios import utils
 from helios.datatypes.djangofield import LDObjectField
 # useful stuff in helios_auth
 from helios_auth.jsonfield import JSONField
 from helios_auth.models import User, AUTH_SYSTEMS
+from .crypto import algs
+from .crypto.elgamal import Cryptosystem
+from .crypto.utils import random, hash_b64
 
 
 class HeliosModel(models.Model, datatypes.LDObjectContainer):
@@ -182,18 +182,18 @@ class Election(HeliosModel):
     if not self.use_voter_aliases:
       return None
     
-    return heliosutils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0
+    return utils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0
 
   @property
   def encrypted_tally_hash(self):
     if not self.encrypted_tally:
       return None
 
-    return utils.hash_b64(self.encrypted_tally.toJSON())
+    return hash_b64(self.encrypted_tally.toJSON())
 
   @property
   def is_archived(self):
-    return self.archived_at != None
+    return self.archived_at is not None
 
   @property
   def description_bleached(self):
@@ -210,9 +210,9 @@ class Election(HeliosModel):
   @classmethod
   def get_by_user_as_admin(cls, user, archived_p=None, limit=None):
     query = cls.objects.filter(admin = user)
-    if archived_p == True:
+    if archived_p is True:
       query = query.exclude(archived_at= None)
-    if archived_p == False:
+    if archived_p is False:
       query = query.filter(archived_at= None)
     query = query.order_by('-created_at')
     if limit:
@@ -223,9 +223,9 @@ class Election(HeliosModel):
   @classmethod
   def get_by_user_as_voter(cls, user, archived_p=None, limit=None):
     query = cls.objects.filter(voter__user = user)
-    if archived_p == True:
+    if archived_p is True:
       query = query.exclude(archived_at= None)
-    if archived_p == False:
+    if archived_p is False:
       query = query.filter(archived_at= None)
     query = query.order_by('-created_at')
     if limit:
@@ -286,7 +286,7 @@ class Election(HeliosModel):
     if not self.openreg:
       return False
     
-    if self.eligibility == None:
+    if self.eligibility is None:
       return True
       
     # is the user eligible for one of these cases?
@@ -301,7 +301,7 @@ class Election(HeliosModel):
       return []
 
     # constraints that are relevant
-    relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and constraint.has_key('constraint')]
+    relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and 'constraint' in constraint]
     if len(relevant_constraints) > 0:
       return relevant_constraints[0]
     else:
@@ -327,7 +327,7 @@ class Election(HeliosModel):
       return_val = "<ul>"
       
       for constraint in self.eligibility:
-        if constraint.has_key('constraint'):
+        if 'constraint' in constraint:
           for one_constraint in constraint['constraint']:
             return_val += "<li>%s</li>" % AUTH_SYSTEMS[constraint['auth_system']].pretty_eligibility(one_constraint)
         else:
@@ -341,7 +341,7 @@ class Election(HeliosModel):
     """
     has voting begun? voting begins if the election is frozen, at the prescribed date or at the date that voting was forced to start
     """
-    return self.frozen_at != None and (self.voting_starts_at == None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at)))
+    return self.frozen_at is not None and (self.voting_starts_at is None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at)))
     
   def voting_has_stopped(self):
     """
@@ -349,12 +349,12 @@ class Election(HeliosModel):
     or failing that the date voting was extended until, or failing that the date voting is scheduled to end at.
     """
     voting_end = self.voting_ended_at or self.voting_extended_until or self.voting_ends_at
-    return (voting_end != None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally
+    return (voting_end is not None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally
 
   @property
   def issues_before_freeze(self):
     issues = []
-    if self.questions == None or len(self.questions) == 0:
+    if self.questions is None or len(self.questions) == 0:
       issues.append(
         {'type': 'questions',
          'action': "add questions to the ballot"}
@@ -368,7 +368,7 @@ class Election(HeliosModel):
           })
 
     for t in trustees:
-      if t.public_key == None:
+      if t.public_key is None:
         issues.append({
             'type': 'trustee keypairs',
             'action': 'have trustee %s generate a keypair' % t.name
@@ -397,8 +397,8 @@ class Election(HeliosModel):
     self.save()    
   
   def ready_for_decryption(self):
-    return self.encrypted_tally != None
-    
+    return self.encrypted_tally is not None
+
   def ready_for_decryption_combination(self):
     """
     do we have a tally from all trustees?
@@ -446,7 +446,7 @@ class Election(HeliosModel):
     else:
       voters = Voter.get_by_election(self)
       voters_json = utils.to_json([v.toJSONDict() for v in voters])
-      self.voters_hash = utils.hash_b64(voters_json)
+      self.voters_hash = hash_b64(voters_json)
     
   def increment_voters(self):
     ## FIXME
@@ -470,7 +470,7 @@ class Election(HeliosModel):
     """
 
     # don't override existing eligibility
-    if self.eligibility != None:
+    if self.eligibility is not None:
       return
 
     # enable this ONLY once the cast_confirm screen makes sense
@@ -478,7 +478,8 @@ class Election(HeliosModel):
     #  return
 
     auth_systems = copy.copy(settings.AUTH_ENABLED_AUTH_SYSTEMS)
-    voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if r['user__user_type'] != None]
+    voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if
+                   r['user__user_type'] is not None]
 
     # password is now separate, not an explicit voter type
     if self.voter_set.filter(user=None).count() > 0:
@@ -555,7 +556,7 @@ class Election(HeliosModel):
       return None
     
   def has_helios_trustee(self):
-    return self.get_helios_trustee() != None
+    return self.get_helios_trustee() is not None
 
   def helios_trustee_decrypt(self):
     tally = self.encrypted_tally
@@ -599,7 +600,7 @@ class Election(HeliosModel):
     determining the winner for one question
     """
     # sort the answers , keep track of the index
-    counts = sorted(enumerate(result), key=lambda(x): x[1])
+    counts = sorted(enumerate(result), key=lambda x: x[1])
     counts.reverse()
     
     the_max = question['max'] or 1
@@ -682,9 +683,9 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
     for row in csv_reader:
       # decode UTF-8 back to Unicode, cell by cell:
       try:
-        yield [unicode(cell, 'utf-8') for cell in row]
+        yield [str(cell, 'utf-8') for cell in row]
       except:
-        yield [unicode(cell, 'latin-1') for cell in row]        
+        yield [str(cell, 'latin-1') for cell in row]        
 
 def utf_8_encoder(unicode_csv_data):
     for line in unicode_csv_data:
@@ -715,20 +716,25 @@ class VoterFile(models.Model):
 
   def itervoters(self):
     if self.voter_file_content:
-      if type(self.voter_file_content) == unicode:
-        content = self.voter_file_content.encode('utf-8')
-      else:
+      if isinstance(self.voter_file_content, str):
+        content = self.voter_file_content.encode(encoding='utf-8')
+      elif isinstance(self.voter_file_content, bytes):
         content = self.voter_file_content
+      else:
+        raise TypeError("voter_file_content is of type {0} instead of str or bytes"
+                        .format(str(type(self.voter_file_content))))
 
       # now we have to handle non-universal-newline stuff
       # we do this in a simple way: replace all \r with \n
       # then, replace all double \n with single \n
       # this should leave us with only \n
-      content = content.replace('\r','\n').replace('\n\n','\n')
+      content = content.replace(b'\r',b'\n').replace(b'\n\n',b'\n')
 
+      close = False
       voter_stream = io.BytesIO(content)
     else:
-      voter_stream = open(self.voter_file.path, "rU")
+      close = True
+      voter_stream = open(self.voter_file.path, "r")
 
     #reader = unicode_csv_reader(voter_stream)
     reader = unicodecsv.reader(voter_stream, encoding='utf-8')
@@ -752,6 +758,8 @@ class VoterFile(models.Model):
         return_dict['name'] = return_dict['email']
 
       yield return_dict
+    if close:
+      voter_stream.close()
     
   def process(self):
     self.processing_started_at = datetime.datetime.utcnow()
@@ -778,7 +786,7 @@ class VoterFile(models.Model):
         existing_voter.save()
 
     if election.use_voter_aliases:
-      voter_alias_integers = range(last_alias_num+1, last_alias_num+1+num_voters)
+      voter_alias_integers = list(range(last_alias_num+1, last_alias_num+1+num_voters))
       random.shuffle(voter_alias_integers)
       for i, voter in enumerate(new_voters):
         voter.alias = 'V%s' % voter_alias_integers[i]
@@ -816,8 +824,7 @@ class Voter(HeliosModel):
   alias = models.CharField(max_length = 100, null=True)
   
   # we keep a copy here for easy tallying
-  vote = LDObjectField(type_hint = 'legacy/EncryptedVote',
-                       null=True)
+  vote = LDObjectField(type_hint = 'legacy/EncryptedVote', null=True)
   vote_hash = models.CharField(max_length = 100, null=True)
   cast_at = models.DateTimeField(auto_now_add=False, null=True)
 
@@ -840,7 +847,7 @@ class Voter(HeliosModel):
 
     # do we need to generate an alias?
     if election.use_voter_aliases:
-      heliosutils.lock_row(Election, election.id)
+      utils.lock_row(Election, election.id)
       alias_num = election.last_alias_num + 1
       voter.alias = "V%s" % alias_num
 
@@ -856,14 +863,14 @@ class Voter(HeliosModel):
     
     # the boolean check is not stupid, this is ternary logic
     # none means don't care if it's cast or not
-    if cast == True:
+    if cast is True:
       query = query.exclude(cast_at = None)
-    elif cast == False:
+    elif cast is False:
       query = query.filter(cast_at = None)
 
     # little trick to get around GAE limitation
     # order by uuid only when no inequality has been added
-    if cast == None or order_by == 'cast_at' or order_by =='-cast_at':
+    if cast is None or order_by == 'cast_at' or order_by == '-cast_at':
       query = query.order_by(order_by)
       
       # if we want the list after a certain UUID, add the inequality here
@@ -947,12 +954,12 @@ class Voter(HeliosModel):
       value_to_hash = self.voter_id
 
     try:
-      return utils.hash_b64(value_to_hash)
+      return hash_b64(value_to_hash)
     except:
       try:
-        return utils.hash_b64(value_to_hash.encode('latin-1'))
+        return hash_b64(value_to_hash.encode('latin-1'))
       except:
-        return utils.hash_b64(value_to_hash.encode('utf-8'))        
+        return hash_b64(value_to_hash.encode('utf-8'))        
 
   @property
   def voter_type(self):
@@ -972,7 +979,7 @@ class Voter(HeliosModel):
     if self.voter_password:
       raise Exception("password already exists")
     
-    self.voter_password = heliosutils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
+    self.voter_password = utils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')
 
   def store_vote(self, cast_vote):
     # only store the vote if it's cast later than the current one
@@ -1166,7 +1173,7 @@ class Trustee(HeliosModel):
     """
     # not saved yet?
     if not self.secret:
-      self.secret = heliosutils.random_string(12)
+      self.secret = utils.random_string(12)
       self.election.append_log("Trustee %s added" % self.name)
       
     super(Trustee, self).save(*args, **kwargs)
diff --git a/helios/security.py b/helios/security.py
index 88dc6754d0bea4e046b6a3e2bb3f8e7c13bb63b5..2f0852d95eb92485c155f359463162b665c1e00a 100644
--- a/helios/security.py
+++ b/helios/security.py
@@ -4,21 +4,19 @@ Helios Security -- mostly access control
 Ben Adida (ben@adida.net)
 """
 
+import urllib.parse
 # nicely update the wrapper function
 from functools import update_wrapper
 
-from django.urls import reverse
+from django.conf import settings
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
-from django.conf import settings
-
-from models import Voter, Trustee, Election
-from helios_auth.security import get_user
-
 from django.http import HttpResponseRedirect
-import urllib
+from django.urls import reverse
 
 import helios
+from helios_auth.security import get_user
+from .models import Voter, Trustee, Election
 
 
 class HSTSMiddleware:
@@ -45,7 +43,7 @@ def get_voter(request, user, election):
   return the current voter
   """
   voter = None
-  if request.session.has_key('CURRENT_VOTER_ID'):
+  if 'CURRENT_VOTER_ID' in request.session:
     voter = Voter.objects.get(id=request.session['CURRENT_VOTER_ID'])
     if voter.election != election:
       voter = None
@@ -59,7 +57,7 @@ def get_voter(request, user, election):
 # a function to check if the current user is a trustee
 HELIOS_TRUSTEE_UUID = 'helios_trustee_uuid'
 def get_logged_in_trustee(request):
-  if request.session.has_key(HELIOS_TRUSTEE_UUID):
+  if HELIOS_TRUSTEE_UUID in request.session:
     return Trustee.get_by_uuid(request.session[HELIOS_TRUSTEE_UUID])
   else:
     return None
@@ -72,26 +70,26 @@ def set_logged_in_trustee(request, trustee):
 #
 def do_election_checks(election, props):
   # frozen
-  if props.has_key('frozen'):
+  if 'frozen' in props:
     frozen = props['frozen']
   else:
     frozen = None
   
   # newvoters (open for registration)
-  if props.has_key('newvoters'):
+  if 'newvoters' in props:
     newvoters = props['newvoters']
   else:
     newvoters = None
   
   # frozen check
-  if frozen != None:
+  if frozen is not None:
     if frozen and not election.frozen_at:
       raise PermissionDenied()
     if not frozen and election.frozen_at:
       raise PermissionDenied()
     
   # open for new voters check
-  if newvoters != None:
+  if newvoters is not None:
     if election.can_add_voters() != newvoters:
       raise PermissionDenied()
 
@@ -120,10 +118,10 @@ def election_view(**checks):
 
       # if private election, only logged in voters
       if election.private_p and not checks.get('allow_logins',False):
-        from views import password_voter_login
+        from .views import password_voter_login
         if not user_can_see_election(request, election):
           return_url = request.get_full_path()
-          return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.urlencode({
+          return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.parse.urlencode({
                   'return_url' : return_url
                   })))
     
@@ -158,11 +156,13 @@ def user_can_see_election(request, election):
     return True
 
   # then this user has to be a voter
-  return (get_voter(request, user, election) != None)
+  return get_voter(request, user, election) is not None
+
 
 def api_client_can_admin_election(api_client, election):
-  return election.api_client == api_client and api_client != None
-  
+  return election.api_client == api_client and api_client is not None
+
+
 # decorator for checking election admin access, and some properties of the election
 # frozen - is the election frozen
 # newvoters - does the election accept new voters
diff --git a/helios/stats_views.py b/helios/stats_views.py
index 5e71a32e8dca7cf20a191aeffe9cc8dc5bda3492..e506637fa7b8ba224bf96ce6a544758dbdaacd70 100644
--- a/helios/stats_views.py
+++ b/helios/stats_views.py
@@ -12,8 +12,8 @@ from django.http import HttpResponseRedirect
 from helios import tasks, url_names
 from helios.models import CastVote, Election
 from helios_auth.security import get_user
-from security import PermissionDenied
-from view_utils import render_template
+from .security import PermissionDenied
+from .view_utils import render_template
 
 
 def require_admin(request):
diff --git a/helios/tasks.py b/helios/tasks.py
index 8610097182707b2aa40abc68e79c148fa664b19d..a1079c1d33c321747d05d5d8b317015f7c928ba1 100644
--- a/helios/tasks.py
+++ b/helios/tasks.py
@@ -8,9 +8,9 @@ import copy
 from celery import shared_task
 from celery.utils.log import get_logger
 
-import signals
-from models import CastVote, Election, Voter, VoterFile
-from view_utils import render_template_raw
+from . import signals
+from .models import CastVote, Election, Voter, VoterFile
+from .view_utils import render_template_raw
 
 
 @shared_task
diff --git a/helios/tests.py b/helios/tests.py
index 157ce2823da9cc332790cea5561cd11d8704730d..7f0c72068dcfba86ee026fb4af5cbd374f66d28a 100644
--- a/helios/tests.py
+++ b/helios/tests.py
@@ -4,10 +4,10 @@ Unit Tests for Helios
 
 import datetime
 import re
-import urllib
+import uuid
+from urllib.parse import urlencode
 
 import django_webtest
-import uuid
 from django.conf import settings
 from django.core import mail
 from django.core.files import File
@@ -53,45 +53,46 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.created_p)
 
         # should have a creation time
-        self.assertNotEquals(self.election.created_at, None)
+        self.assertNotEqual(self.election.created_at, None)
         self.assertTrue(self.election.created_at < datetime.datetime.utcnow())
 
     def test_find_election(self):
         election = models.Election.get_by_user_as_admin(self.user)[0]
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
 
         election = models.Election.get_by_uuid(self.election.uuid)
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
 
         election = models.Election.get_by_short_name(self.election.short_name)
-        self.assertEquals(self.election, election)
+        self.assertEqual(self.election, election)
         
     def test_setup_trustee(self):
         self.setup_trustee()
-        self.assertEquals(self.election.num_trustees, 1)
+        self.assertEqual(self.election.num_trustees, 1)
 
     def test_add_voters_file(self):
         election = self.election
 
         FILE = "helios/fixtures/voter-file.csv"
-        vf = models.VoterFile.objects.create(election = election, voter_file = File(open(FILE), "voter_file.css"))
-        vf.process()
+        with open(FILE, 'r', encoding='utf-8') as f:
+            vf = models.VoterFile.objects.create(election = election, voter_file = File(f, "voter_file.css"))
+            vf.process()
 
         # make sure that we stripped things correctly
         voter = election.voter_set.get(voter_login_id = 'benadida5')
-        self.assertEquals(voter.voter_email, 'ben5@adida.net')
-        self.assertEquals(voter.voter_name, 'Ben5 Adida')
+        self.assertEqual(voter.voter_email, 'ben5@adida.net')
+        self.assertEqual(voter.voter_name, 'Ben5 Adida')
 
     def test_check_issues_before_freeze(self):
         # should be three issues: no trustees, and no questions, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 3)
+        self.assertEqual(len(issues), 3)
 
         self.setup_questions()
 
         # should be two issues: no trustees, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 2)
+        self.assertEqual(len(issues), 2)
 
         self.election.questions = None
 
@@ -99,7 +100,7 @@ class ElectionModelTests(TestCase):
 
         # should be two issues: no questions, and no voters
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 2)
+        self.assertEqual(len(issues), 2)
         
         self.setup_questions()
 
@@ -107,7 +108,7 @@ class ElectionModelTests(TestCase):
         self.setup_openreg()
 
         issues = self.election.issues_before_freeze
-        self.assertEquals(len(issues), 0)
+        self.assertEqual(len(issues), 0)
         
     def test_helios_trustee(self):
         self.election.generate_trustee(views.ELGAMAL_PARAMS)
@@ -115,7 +116,7 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.election.has_helios_trustee())
 
         trustee = self.election.get_helios_trustee()
-        self.assertNotEquals(trustee, None)
+        self.assertNotEqual(trustee, None)
 
     def test_log(self):
         LOGS = ["testing 1", "testing 2", "testing 3"]
@@ -126,7 +127,7 @@ class ElectionModelTests(TestCase):
         pulled_logs = [l.log for l in self.election.get_log().all()]
         pulled_logs.reverse()
 
-        self.assertEquals(LOGS,pulled_logs)
+        self.assertEqual(LOGS,pulled_logs)
 
     def test_eligibility(self):
         self.election.eligibility = [{'auth_system': self.user.user_type}]
@@ -137,7 +138,7 @@ class ElectionModelTests(TestCase):
         # what about after saving?
         self.election.save()
         e = models.Election.objects.get(uuid = self.election.uuid)
-        self.assertEquals(e.eligibility, [{'auth_system': self.user.user_type}])
+        self.assertEqual(e.eligibility, [{'auth_system': self.user.user_type}])
 
         self.election.openreg = True
 
@@ -166,7 +167,7 @@ class ElectionModelTests(TestCase):
         self.assertTrue(self.election.user_eligible_p(self.fb_user))
 
         # also check that eligibility_category_id does the right thing
-        self.assertEquals(self.election.eligibility_category_id('facebook'), '123')
+        self.assertEqual(self.election.eligibility_category_id('facebook'), '123')
 
     def test_freeze(self):
         # freezing without trustees and questions, no good
@@ -194,7 +195,7 @@ class ElectionModelTests(TestCase):
     def test_voter_registration(self):
         # before adding a voter
         voters = models.Voter.get_by_election(self.election)
-        self.assertEquals(0, len(voters))
+        self.assertEqual(0, len(voters))
 
         # make sure no voter yet
         voter = models.Voter.get_by_election_and_user(self.election, self.user)
@@ -202,7 +203,7 @@ class ElectionModelTests(TestCase):
 
         # make sure no voter at all across all elections
         voters = models.Voter.get_by_user(self.user)
-        self.assertEquals(0, len(voters))
+        self.assertEqual(0, len(voters))
 
         # register the voter
         voter = models.Voter.register_user_in_election(self.user, self.election)
@@ -212,17 +213,17 @@ class ElectionModelTests(TestCase):
 
         self.assertIsNotNone(voter)
         self.assertIsNotNone(voter_2)
-        self.assertEquals(voter, voter_2)
+        self.assertEqual(voter, voter_2)
 
         # make sure voter is there in this call too
         voters = models.Voter.get_by_user(self.user)
-        self.assertEquals(1, len(voters))
-        self.assertEquals(voter, voters[0])
+        self.assertEqual(1, len(voters))
+        self.assertEqual(voter, voters[0])
 
         voter_2 = models.Voter.get_by_election_and_uuid(self.election, voter.uuid)
-        self.assertEquals(voter, voter_2)
+        self.assertEqual(voter, voter_2)
 
-        self.assertEquals(voter.user, self.user)
+        self.assertEqual(voter.user, self.user)
 
 
 
@@ -241,13 +242,13 @@ class VoterModelTests(TestCase):
         v.save()
         
         # password has been generated!
-        self.assertFalse(v.voter_password == None)
+        self.assertFalse(v.voter_password is None)
 
         # can't generate passwords twice
         self.assertRaises(Exception, lambda: v.generate_password())
         
         # check that you can get at the voter user structure
-        self.assertEquals(v.get_user().user_id, v.voter_email)
+        self.assertEqual(v.get_user().user_id, v.voter_email)
 
 
 class CastVoteModelTests(TestCase):
@@ -289,7 +290,7 @@ class DatatypeTests(TestCase):
             'B' : '234324243'}
         ld_obj = datatypes.LDObject.fromDict(original_dict, type_hint = 'legacy/EGZKProofCommitment')
 
-        self.assertEquals(original_dict, ld_obj.toDict())
+        self.assertEqual(original_dict, ld_obj.toDict())
         
         
         
@@ -303,9 +304,8 @@ class DataFormatBlackboxTests(object):
         self.election = models.Election.objects.all()[0]
 
     def assertEqualsToFile(self, response, file_path):
-        expected = open(file_path)
-        self.assertEquals(response.content, expected.read())
-        expected.close()
+        with open(file_path) as expected:
+            self.assertEqual(response.content, expected.read().encode('utf-8'))
 
     def test_election(self):
         response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
@@ -349,10 +349,8 @@ class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase):
 
 class WebTest(django_webtest.WebTest):
     def assertStatusCode(self, response, status_code):
-        if hasattr(response, 'status_code'):
-            assert response.status_code == status_code, response.status_code
-        else:
-            assert response.status_int == status_code, response.status_int
+        actual_code = response.status_code if hasattr(response, 'status_code') else response.status_int
+        assert actual_code == status_code, "%s instad of %s" % (actual_code, status_code)
 
 
     def assertRedirects(self, response, url):
@@ -374,11 +372,13 @@ class WebTest(django_webtest.WebTest):
         self.assertStatusCode(response, 200)
 
         if hasattr(response, "testbody"):
-            assert text in response.testbody, "missing text %s" % text
+            t = response.testbody
         elif hasattr(response, "body"):
-            assert text in response.body, "missing text %s" % text
+            t = response.body
         else:
-            assert text in response.content, "missing text %s" % text
+            t = response.content
+
+        assert text in str(t), "missing text %s" % text
 
 
 ##
@@ -418,7 +418,7 @@ class ElectionBlackboxTests(WebTest):
 
     def test_election_params(self):
         response = self.client.get("/helios/elections/params")
-        self.assertEquals(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize())
+        self.assertEqual(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize().encode('utf-8'))
 
     def test_election_404(self):
         response = self.client.get("/helios/elections/foobar")
@@ -434,7 +434,7 @@ class ElectionBlackboxTests(WebTest):
         
     def test_get_election_raw(self):
         response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False)
-        self.assertEquals(response.content, self.election.toJSON())
+        self.assertEqual(response.content, self.election.toJSON().encode('utf-8'))
     
     def test_get_election(self):
         response = self.client.get("/helios/elections/%s/view" % self.election.uuid, follow=False)
@@ -460,7 +460,7 @@ class ElectionBlackboxTests(WebTest):
 
     def test_get_election_voters_raw(self):
         response = self.client.get("/helios/elections/%s/voters/" % self.election.uuid, follow=False)
-        self.assertEquals(len(utils.from_json(response.content)), self.election.num_voters)
+        self.assertEqual(len(utils.from_json(response.content)), self.election.num_voters)
         
     def test_election_creation_not_logged_in(self):
         response = self.client.post("/helios/elections/new", {
@@ -489,7 +489,7 @@ class ElectionBlackboxTests(WebTest):
         self.assertRedirects(response, "/helios/elections/%s/view" % self.election.uuid)
 
         new_election = models.Election.objects.get(uuid = self.election.uuid)
-        self.assertEquals(new_election.short_name, self.election.short_name + "-2")
+        self.assertEqual(new_election.short_name, self.election.short_name + "-2")
 
     def test_get_election_stats(self):
         self.setup_login(from_scratch=True, user_id='mccio@github.com', user_type='google')
@@ -569,7 +569,7 @@ class ElectionBlackboxTests(WebTest):
         # and we want to check that there are now voters
         response = self.client.get("/helios/elections/%s/voters/" % election_id)
         NUM_VOTERS = 4
-        self.assertEquals(len(utils.from_json(response.content)), NUM_VOTERS)
+        self.assertEqual(len(utils.from_json(response.content)), NUM_VOTERS)
 
         # let's get a single voter
         single_voter = models.Election.objects.get(uuid = election_id).voter_set.all()[0]
@@ -602,7 +602,7 @@ class ElectionBlackboxTests(WebTest):
                 })
         self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
         num_messages_after = len(mail.outbox)
-        self.assertEquals(num_messages_after - num_messages_before, NUM_VOTERS)
+        self.assertEqual(num_messages_after - num_messages_before, NUM_VOTERS)
 
         email_message = mail.outbox[num_messages_before]
         assert "your password" in email_message.subject, "bad subject in email"
@@ -613,7 +613,7 @@ class ElectionBlackboxTests(WebTest):
 
         # now log out as administrator
         self.clear_login()
-        self.assertEquals(self.client.session.has_key('user'), False)
+        self.assertEqual('user' in self.client.session, False)
 
         # return the voter username and password to vote
         return election_id, username, password
@@ -629,7 +629,7 @@ class ElectionBlackboxTests(WebTest):
 
         # parse it as an encrypted vote with randomness, and make sure randomness is there
         the_ballot = utils.from_json(response.testbody)
-        assert the_ballot['answers'][0].has_key('randomness'), "no randomness"
+        assert 'randomness' in the_ballot['answers'][0], "no randomness"
         assert len(the_ballot['answers'][0]['randomness']) == 2, "not enough randomness"
         
         # parse it as an encrypted vote, and re-serialize it
@@ -661,7 +661,7 @@ class ElectionBlackboxTests(WebTest):
             # confirm the vote, now with the actual form
             cast_form = cast_confirm_page.form
         
-            if 'status_update' in cast_form.fields.keys():
+            if 'status_update' in list(cast_form.fields.keys()):
                 cast_form['status_update'] = False
             response = cast_form.submit()
 
@@ -711,7 +711,7 @@ class ElectionBlackboxTests(WebTest):
         self.assertRedirects(response, "/helios/elections/%s/view" % election_id)
 
         # should trigger helios decryption automatically
-        self.assertNotEquals(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
+        self.assertNotEqual(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None)
 
         # combine decryptions
         response = self.client.post("/helios/elections/%s/combine_decryptions" % election_id, {
@@ -732,7 +732,7 @@ class ElectionBlackboxTests(WebTest):
 
         # check that tally matches
         response = self.client.get("/helios/elections/%s/result" % election_id)
-        self.assertEquals(utils.from_json(response.content), [[0,1]])
+        self.assertEqual(utils.from_json(response.content), [[0,1]])
         
     def test_do_complete_election(self):
         election_id, username, password = self._setup_complete_election()
@@ -761,7 +761,7 @@ class ElectionBlackboxTests(WebTest):
         response = self.app.get("/helios/elections/%s/view" % election_id)
 
         # ensure it redirects
-        self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urllib.urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
+        self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urlencode({"return_url": "/helios/elections/%s/view" % election_id})))
 
         login_form = response.follow().form
 
diff --git a/helios/urls.py b/helios/urls.py
index 6d278a9d195f17b169f4d7f126ef32ed3c012c61..9183eedd1fdc4e2c7fd89535b557d972106fc216 100644
--- a/helios/urls.py
+++ b/helios/urls.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 from django.conf.urls import url, include
 
-import url_names as names
-import views
+from . import views, url_names as names
 
 urlpatterns = [
   url(r'^autologin$', views.admin_autologin),
diff --git a/helios/utils.py b/helios/utils.py
index 2ddc11c733f026a272b9b0f27b5cf54afae35bf2..0080c5f0a3d979d0efd6854f9e7304ec62bd43d1 100644
--- a/helios/utils.py
+++ b/helios/utils.py
@@ -5,14 +5,17 @@ Ben Adida - ben@adida.net
 2005-04-11
 """
 
-import urllib, re, datetime, string
+import datetime
+import re
+import string
+import urllib.parse
+
+from django.conf import settings
 
 from helios.crypto.utils import random
 # utils from helios_auth, too
 from helios_auth.utils import *
 
-from django.conf import settings
-
 
 def split_by_length(str, length, rejoin_with=None):
   """
@@ -37,7 +40,7 @@ def urlencode(str):
     if not str:
         return ""
 
-    return urllib.quote(str)
+    return urllib.parse.quote(str)
 
 def urlencodeall(str):
     """
@@ -52,11 +55,11 @@ def urldecode(str):
     if not str:
         return ""
 
-    return urllib.unquote(str)
+    return urllib.parse.unquote(str)
 
 def dictToURLParams(d):
   if d:
-    return '&'.join([i + '=' + urlencode(v) for i,v in d.items()])
+    return '&'.join([i + '=' + urlencode(v) for i,v in list(d.items())])
   else:
     return None
 ##
@@ -86,22 +89,22 @@ def xss_strip_all_tags(s):
         if text[:2] == "&#":
             try:
                 if text[:3] == "&#x":
-                    return unichr(int(text[3:-1], 16))
+                    return chr(int(text[3:-1], 16))
                 else:
-                    return unichr(int(text[2:-1]))
+                    return chr(int(text[2:-1]))
             except ValueError:
                 pass
         elif text[:1] == "&":
-            import htmlentitydefs
-            entity = htmlentitydefs.entitydefs.get(text[1:-1])
+            import html.entities
+            entity = html.entities.entitydefs.get(text[1:-1])
             if entity:
                 if entity[:2] == "&#":
                     try:
-                        return unichr(int(entity[2:-1]))
+                        return chr(int(entity[2:-1]))
                     except ValueError:
                         pass
                 else:
-                    return unicode(entity, "iso-8859-1")
+                    return str(entity, "iso-8859-1")
         return text # leave as is
         
     return re.sub("(?s)<[^>]*>|&#?\w+;", fixup, s)
@@ -127,7 +130,7 @@ def get_prefix():
 ##
 
 def string_to_datetime(str, fmt="%Y-%m-%d %H:%M"):
-  if str == None:
+  if str is None:
     return None
 
   return datetime.datetime.strptime(str, fmt)
diff --git a/helios/view_utils.py b/helios/view_utils.py
index 26da7046c50d1f6c8d62e256cd67175f76cf141f..1c8c9a9011e44404633dcfd17e5706ce86bdb2b3 100644
--- a/helios/view_utils.py
+++ b/helios/view_utils.py
@@ -12,7 +12,7 @@ from django.template import loader
 from functools import update_wrapper
 
 import helios
-import utils
+from . import utils
 from helios_auth.security import get_user
 
 ##
@@ -32,7 +32,7 @@ def prepare_vars(request, values):
   vars_with_user['user'] = get_user(request)
 
   # csrf protection
-  if request.session.has_key('csrf_token'):
+  if 'csrf_token' in request.session:
     vars_with_user['csrf_token'] = request.session['csrf_token']
 
   vars_with_user['utils'] = utils
@@ -67,7 +67,7 @@ def render_template_raw(request, template_name, values=None):
 
 
 def render_json(json_txt):
-  return HttpResponse(json_txt, "application/json")
+  return HttpResponse(utils.to_json(json_txt), content_type="application/json")
 
 
 # decorator
@@ -79,8 +79,8 @@ def return_json(func):
     def convert_to_json(self, *args, **kwargs):
       return_val = func(self, *args, **kwargs)
       try:
-        return render_json(utils.to_json(return_val))
-      except Exception, e:
+        return render_json(return_val)
+      except Exception as e:
         import logging
         logging.error("problem with serialization: " + str(return_val) + " / " + str(e))
         raise e
diff --git a/helios/views.py b/helios/views.py
index b62f6eebf73f14a748fdcd84a61d109394fb263d..ac52823a85e8f54cef45e7bbdec345e2f02d565a 100644
--- a/helios/views.py
+++ b/helios/views.py
@@ -5,51 +5,46 @@ Helios Django Views
 Ben Adida (ben@adida.net)
 """
 
-from django.urls import reverse
-from django.core.paginator import Paginator
+import base64
+import datetime
+import logging
+import os
+import uuid
+from urllib.parse import urlencode
+
 from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden
+from django.core.paginator import Paginator
 from django.db import transaction, IntegrityError
-
+from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden
+from django.urls import reverse
 from validate_email import validate_email
 
-import urllib, os, base64
-
-from crypto import algs, electionalgs, elgamal
-from crypto import utils as cryptoutils
-from workflows import homomorphic
+import helios_auth.url_names as helios_auth_urls
 from helios import utils, VOTERS_EMAIL, VOTERS_UPLOAD, url_names
-from view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw
-
-from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts
+from helios_auth import views as auth_views
 from helios_auth.auth_systems import AUTH_SYSTEMS, can_list_categories
 from helios_auth.models import AuthenticationExpired
-import helios_auth.url_names as helios_auth_urls
-
-from helios_auth import views as auth_views
-
-import tasks
-
-from security import (election_view, election_admin,
-                      trustee_check, set_logged_in_trustee,
-                      can_create_election, user_can_see_election, get_voter,
-                      user_can_admin_election, user_can_feature_election)
-
-import uuid, datetime
-import logging
-
-from models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot
-import datatypes
-
-import forms
+from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts
+from . import datatypes
+from . import forms
+from . import tasks
+from .crypto import algs, electionalgs, elgamal
+from .crypto import utils as cryptoutils
+from .models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot
+from .security import (election_view, election_admin,
+                       trustee_check, set_logged_in_trustee,
+                       can_create_election, user_can_see_election, get_voter,
+                       user_can_admin_election, user_can_feature_election)
+from .view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw
+from .workflows import homomorphic
 
 # Parameters for everything
 ELGAMAL_PARAMS = elgamal.Cryptosystem()
 
 # trying new ones from OlivierP
-ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071L
-ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917L
-ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533L
+ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071
+ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917
+ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533
 
 # object ready for serialization
 ELGAMAL_PARAMS_LD_OBJECT = datatypes.LDObject.instantiate(ELGAMAL_PARAMS, datatype='legacy/EGParams')
@@ -78,7 +73,7 @@ def user_reauth(request, user):
   # add a parameter to prevent it? Maybe.
   login_url = "%s%s?%s" % (settings.SECURE_URL_HOST,
                            reverse(helios_auth_urls.AUTH_START, args=[user.user_type]),
-                           urllib.urlencode({'return_url':
+                           urlencode({'return_url':
                                                request.get_full_path()}))
   return HttpResponseRedirect(login_url)
 
@@ -124,9 +119,9 @@ def election_shortcut(request, election_short_name):
 # a hidden view behind the shortcut that performs the actual perm check
 @election_view()
 def _election_vote_shortcut(request, election):
-  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
+  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
   
-  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url}))
+  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
 
   return HttpResponseRedirect(test_cookie_url)
   
@@ -304,9 +299,9 @@ def one_election_view(request, election):
   election_badge_url = get_election_badge_url(election)
   status_update_message = None
 
-  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
+  vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])}))
 
-  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url}))
+  test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url}))
   
   if user:
     voter = Voter.get_by_election_and_user(election, user)
@@ -329,13 +324,13 @@ def one_election_view(request, election):
   # status update message?
   if election.openreg:
     if election.voting_has_started:
-      status_update_message = u"Vote in %s" % election.name
+      status_update_message = "Vote in %s" % election.name
     else:
-      status_update_message = u"Register to vote in %s" % election.name
+      status_update_message = "Register to vote in %s" % election.name
 
   # result!
   if election.result:
-    status_update_message = u"Results are in for %s" % election.name
+    status_update_message = "Results are in for %s" % election.name
   
   trustees = Trustee.get_by_election(election)
 
@@ -353,20 +348,20 @@ def one_election_view(request, election):
 def test_cookie(request):
   continue_url = request.GET['continue_url']
   request.session.set_test_cookie()
-  next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urllib.urlencode({'continue_url': continue_url}))
+  next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urlencode({'continue_url': continue_url}))
   return HttpResponseRedirect(settings.SECURE_URL_HOST + next_url)  
 
 def test_cookie_2(request):
   continue_url = request.GET['continue_url']
 
   if not request.session.test_cookie_worked():
-    return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urllib.urlencode({'continue_url': continue_url}))))
+    return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urlencode({'continue_url': continue_url}))))
 
   request.session.delete_test_cookie()
   return HttpResponseRedirect(continue_url)  
 
 def nocookies(request):
-  retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : request.GET['continue_url']}))
+  retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : request.GET['continue_url']}))
   return render_template(request, 'nocookies', {'retest_url': retest_url})
 
 ##
@@ -593,7 +588,7 @@ def password_voter_login(request, election):
         return one_election_cast_confirm(request, election.uuid)
       
     except Voter.DoesNotExist:
-      redirect_url = login_url + "?" + urllib.urlencode({
+      redirect_url = login_url + "?" + urlencode({
           'bad_voter_login' : '1',
           'return_url' : return_url
           })
@@ -601,7 +596,7 @@ def password_voter_login(request, election):
       return HttpResponseRedirect(settings.SECURE_URL_HOST + redirect_url)
   else:
     # bad form, bad voter login
-    redirect_url = login_url + "?" + urllib.urlencode({
+    redirect_url = login_url + "?" + urlencode({
         'bad_voter_login' : '1',
         'return_url' : return_url
         })
@@ -615,7 +610,7 @@ def one_election_cast_confirm(request, election):
   user = get_user(request)    
 
   # if no encrypted vote, the user is reloading this page or otherwise getting here in a bad way
-  if (not request.session.has_key('encrypted_vote')) or request.session['encrypted_vote'] == None:
+  if ('encrypted_vote' not in request.session) or request.session['encrypted_vote'] is None:
     return HttpResponseRedirect(settings.URL_HOST)
 
   # election not frozen or started
@@ -693,7 +688,7 @@ def one_election_cast_confirm(request, election):
 
     password_only = False
 
-    if auth_systems == None or 'password' in auth_systems:
+    if auth_systems is None or 'password' in auth_systems:
       show_password = True
       password_login_form = forms.VoterPasswordForm()
 
@@ -763,7 +758,7 @@ def one_election_cast_done(request, election):
     # only log out if the setting says so *and* we're dealing
     # with a site-wide voter. Definitely remove current_voter
     # checking that voter.user != None is needed because voter.user may now be None if voter is password only
-    if voter.user == user and voter.user != None:
+    if voter.user == user and voter.user is not None:
       logout = settings.LOGOUT_ON_CONFIRMATION
     else:
       logout = False
@@ -819,7 +814,7 @@ def one_election_bboard(request, election):
     order_by = 'alias'
 
   # if there's a specific voter
-  if request.GET.has_key('q'):
+  if 'q' in request.GET:
     # FIXME: figure out the voter by voter_id
     voters = []
   else:
@@ -843,7 +838,7 @@ def one_election_audited_ballots(request, election):
   UI to show election audited ballots
   """
   
-  if request.GET.has_key('vote_hash'):
+  if 'vote_hash' in request.GET:
     b = AuditedBallot.get(election, request.GET['vote_hash'])
     return HttpResponse(b.raw_vote, content_type="text/plain")
     
@@ -1082,14 +1077,14 @@ def one_election_compute_tally(request, election):
 
 @trustee_check
 def trustee_decrypt_and_prove(request, election, trustee):
-  if not _check_election_tally_type(election) or election.encrypted_tally == None:
+  if not _check_election_tally_type(election) or election.encrypted_tally is None:
     return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.uuid]))
     
   return render_template(request, 'trustee_decrypt_and_prove', {'election': election, 'trustee': trustee})
   
 @election_view(frozen=True)
 def trustee_upload_decryption(request, election, trustee_uuid):
-  if not _check_election_tally_type(election) or election.encrypted_tally == None:
+  if not _check_election_tally_type(election) or election.encrypted_tally is None:
     return FAILURE
 
   trustee = Trustee.get_by_election_and_uuid(election, trustee_uuid)
@@ -1130,7 +1125,7 @@ def release_result(request, election):
     election.save()
 
     if request.POST.get('send_email', ''):
-      return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urllib.urlencode({'template': 'result'})))
+      return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urlencode({'template': 'result'})))
     else:
       return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid]))
 
@@ -1293,7 +1288,7 @@ def voters_upload(request, election):
       return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid]))
     else:
       # we need to confirm
-      if request.FILES.has_key('voters_file'):
+      if 'voters_file' in request.FILES:
         voters_file = request.FILES['voters_file']
         voter_file_obj = election.add_voters_file(voters_file)
 
@@ -1314,7 +1309,7 @@ def voters_upload(request, election):
 
         return render_template(request, 'voters_upload_confirm', {'election': election, 'voters': voters, 'problems': problems})
       else:
-        return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urllib.urlencode({'e':'no voter file specified, try again'})))
+        return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urlencode({'e':'no voter file specified, try again'})))
 
 @election_admin()
 def voters_upload_cancel(request, election):
@@ -1472,9 +1467,9 @@ def ballot_list(request, election):
   and optionally take a after parameter.
   """
   limit = after = None
-  if request.GET.has_key('limit'):
+  if 'limit' in request.GET:
     limit = int(request.GET['limit'])
-  if request.GET.has_key('after'):
+  if 'after' in request.GET:
     after = datetime.datetime.strptime(request.GET['after'], '%Y-%m-%d %H:%M:%S')
     
   voters = Voter.get_by_election(election, cast=True, order_by='cast_at', limit=limit, after=after)
diff --git a/helios/widgets.py b/helios/widgets.py
index 9eff2f435bc1bf7e37cb8174a5bc1a6db12272f1..e4855a69b6e6b8194e3bd1575079cfc9063c4647 100644
--- a/helios/widgets.py
+++ b/helios/widgets.py
@@ -54,18 +54,18 @@ class SelectTimeWidget(Widget):
             self.meridiem_val = 'a.m.' # Default to Morning (A.M.)
         
         if hour_step and twelve_hr:
-            self.hours = range(1,13,hour_step) 
+            self.hours = list(range(1,13,hour_step)) 
         elif hour_step: # 24hr, with stepping.
-            self.hours = range(0,24,hour_step)
+            self.hours = list(range(0,24,hour_step))
         elif twelve_hr: # 12hr, no stepping
-            self.hours = range(1,13)
+            self.hours = list(range(1,13))
         else: # 24hr, no stepping
-            self.hours = range(0,24) 
+            self.hours = list(range(0,24)) 
 
         if minute_step:
-            self.minutes = range(0,60,minute_step)
+            self.minutes = list(range(0,60,minute_step))
         else:
-            self.minutes = range(0,60)
+            self.minutes = list(range(0,60))
 
     def render(self, name, value, attrs=None, renderer=None):
         try: # try to get time values from a datetime.time object (value)
@@ -77,7 +77,7 @@ class SelectTimeWidget(Widget):
                     self.meridiem_val = 'a.m.'
         except AttributeError:
             hour_val = minute_val = 0
-            if isinstance(value, basestring):
+            if isinstance(value, str):
                 match = RE_TIME.match(value)
                 if match:
                     time_groups = match.groups()
@@ -113,8 +113,8 @@ class SelectTimeWidget(Widget):
 
         # For times to get displayed correctly, the values MUST be converted to unicode
         # When Select builds a list of options, it checks against Unicode values
-        hour_val = u"%.2d" % hour_val
-        minute_val = u"%.2d" % minute_val
+        hour_val = "%.2d" % hour_val
+        minute_val = "%.2d" % minute_val
 
         hour_choices = [("%.2d"%i, "%.2d"%i) for i in self.hours]
         local_attrs = self.build_attrs({'id': self.hour_field % id_})
@@ -137,7 +137,7 @@ class SelectTimeWidget(Widget):
             select_html = Select(choices=meridiem_choices).render(self.meridiem_field % name, self.meridiem_val, local_attrs)
             output.append(select_html)
 
-        return mark_safe(u'\n'.join(output))
+        return mark_safe('\n'.join(output))
 
     def id_for_label(self, id_):
         return '%s_hour' % id_
@@ -189,4 +189,4 @@ class SplitSelectDateTimeWidget(MultiWidget):
 
     def render(self, name, value, attrs=None, renderer=None):
         rendered_widgets = list(widget.render(name, value, attrs=attrs, renderer=renderer) for widget in self.widgets)
-        return u'<br/>'.join(rendered_widgets)
+        return '<br/>'.join(rendered_widgets)
diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py
index 9becf335a1654fc1448385266bd106fd9b57c945..2f03f50ecc6375be6adcc966e350e5c4a1a84924 100644
--- a/helios/workflows/homomorphic.py
+++ b/helios/workflows/homomorphic.py
@@ -68,10 +68,10 @@ class EncryptedAnswer(WorkflowObject):
         return False
 
       # compute homomorphic sum if needed
-      if max != None:
+      if max is not None:
         homomorphic_sum = choice * homomorphic_sum
     
-    if max != None:
+    if max is not None:
       # determine possible plaintexts for the sum
       sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
 
@@ -110,7 +110,7 @@ class EncryptedAnswer(WorkflowObject):
 
     # min and max for number of answers, useful later
     min_answers = 0
-    if question.has_key('min'):
+    if 'min' in question:
       min_answers = question['min']
     max_answers = question['max']
 
@@ -132,7 +132,7 @@ class EncryptedAnswer(WorkflowObject):
                                                 randomness[answer_num], algs.EG_disjunctive_challenge_generator)
                                                 
       # sum things up homomorphically if needed
-      if max_answers != None:
+      if max_answers is not None:
         homomorphic_sum = choices[answer_num] * homomorphic_sum
         randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
 
@@ -142,7 +142,7 @@ class EncryptedAnswer(WorkflowObject):
     if num_selected_answers < min_answers:
       raise Exception("Need to select at least %s answer(s)" % min_answers)
     
-    if max_answers != None:
+    if max_answers is not None:
       sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
     
       # need to subtract the min from the offset
@@ -195,7 +195,7 @@ class EncryptedVote(WorkflowObject):
 
       question = election.questions[question_num]
       min_answers = 0
-      if question.has_key('min'):
+      if 'min' in question:
         min_answers = question['min']
         
       if not ea.verify(election.public_key, min=min_answers, max=question['max']):
diff --git a/helios_auth/__init__.py b/helios_auth/__init__.py
index ca245ff38f820db528afbed3a43d597bc8642b44..2c9c090a97a242a71b7d3d6c177dfa31d3016476 100644
--- a/helios_auth/__init__.py
+++ b/helios_auth/__init__.py
@@ -4,7 +4,7 @@ from django.conf import settings
 TEMPLATE_BASE = settings.AUTH_TEMPLATE_BASE or "helios_auth/templates/base.html"
 
 # enabled auth systems
-import auth_systems
-ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or auth_systems.AUTH_SYSTEMS.keys()
+from . import auth_systems
+ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or list(auth_systems.AUTH_SYSTEMS.keys())
 DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_AUTH_SYSTEM or None
 
diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py
index 5a0e9233ba4df89067688283e90649aef4f1ae70..07ebaa867a322cdfeeb133a9713731b2f02a44fa 100644
--- a/helios_auth/auth_systems/__init__.py
+++ b/helios_auth/auth_systems/__init__.py
@@ -1,15 +1,42 @@
+from django.conf import settings
+
+_enabled = settings.AUTH_ENABLED_AUTH_SYSTEMS or None
+def _is_enabled(system):
+    return _enabled is not None or system in _enabled
 
 AUTH_SYSTEMS = {}
 
-import twitter, password, cas, facebook, google, yahoo, linkedin, clever
-AUTH_SYSTEMS['twitter'] = twitter
-AUTH_SYSTEMS['linkedin'] = linkedin
-AUTH_SYSTEMS['password'] = password
-AUTH_SYSTEMS['cas'] = cas
-AUTH_SYSTEMS['facebook'] = facebook
-AUTH_SYSTEMS['google'] = google
-AUTH_SYSTEMS['yahoo'] = yahoo
-AUTH_SYSTEMS['clever'] = clever
+if _is_enabled('twitter'):
+    from . import twitter
+    AUTH_SYSTEMS['twitter'] = twitter
+
+if _is_enabled('linkedin'):
+    from . import linkedin
+    AUTH_SYSTEMS['linkedin'] = linkedin
+
+if _is_enabled('password'):
+    from . import password
+    AUTH_SYSTEMS['password'] = password
+
+if _is_enabled('cas'):
+    from . import cas
+    AUTH_SYSTEMS['cas'] = cas
+
+if _is_enabled('facebook'):
+    from . import facebook
+    AUTH_SYSTEMS['facebook'] = facebook
+
+if _is_enabled('google'):
+    from . import google
+    AUTH_SYSTEMS['google'] = google
+
+if _is_enabled('yahoo'):
+    from . import yahoo
+    AUTH_SYSTEMS['yahoo'] = yahoo
+
+if _is_enabled('clever'):
+    from . import clever
+    AUTH_SYSTEMS['clever'] = clever
 
 # not ready
 #import live
diff --git a/helios_auth/auth_systems/cas.py b/helios_auth/auth_systems/cas.py
index dd4216b5ecc2c3953892332fa49ae8a51b13eeff..21c86cfe73577021491bacfd1c20f58f36c94fe2 100644
--- a/helios_auth/auth_systems/cas.py
+++ b/helios_auth/auth_systems/cas.py
@@ -7,13 +7,14 @@ https://sp.princeton.edu/oit/sdp/CAS/Wiki%20Pages/Python.aspx
 
 import datetime
 import re
-import urllib
-import urllib2
+import urllib.parse
+import urllib.request
 import uuid
+from xml.etree import ElementTree
+
 from django.conf import settings
 from django.core.mail import send_mail
 from django.http import HttpResponseRedirect
-from xml.etree import ElementTree
 
 CAS_EMAIL_DOMAIN = "princeton.edu"
 CAS_URL= 'https://fed.princeton.edu/cas/'
@@ -42,17 +43,17 @@ def _get_service_url():
   
 def get_auth_url(request, redirect_url):
   request.session['cas_redirect_url'] = redirect_url
-  return CAS_URL + 'login?service=' + urllib.quote(_get_service_url())
+  return CAS_URL + 'login?service=' + urllib.parse.quote(_get_service_url())
 
 def get_user_category(user_id):
   theurl = CAS_ELIGIBILITY_URL % user_id
 
-  auth_handler = urllib2.HTTPBasicAuthHandler()
+  auth_handler = urllib.request.HTTPBasicAuthHandler()
   auth_handler.add_password(realm=CAS_ELIGIBILITY_REALM, uri= theurl, user= CAS_USERNAME, passwd = CAS_PASSWORD)
-  opener = urllib2.build_opener(auth_handler)
-  urllib2.install_opener(opener)
+  opener = urllib.request.build_opener(auth_handler)
+  urllib.request.install_opener(opener)
   
-  result = urllib2.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
+  result = urllib.request.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip()
   parsed_result = ElementTree.fromstring(result)
   return parsed_result.text
   
@@ -78,11 +79,11 @@ def get_saml_info(ticket):
   </soap-env:Envelope>
 """ % (uuid.uuid1(), datetime.datetime.utcnow().isoformat(), ticket)
 
-  url = CAS_SAML_VALIDATE_URL % urllib.quote(_get_service_url())
+  url = CAS_SAML_VALIDATE_URL % urllib.parse.quote(_get_service_url())
 
   # by virtue of having a body, this is a POST
-  req = urllib2.Request(url, saml_request)
-  raw_response = urllib2.urlopen(req).read()
+  req = urllib.request.Request(url, saml_request)
+  raw_response = urllib.request.urlopen(req).read()
 
   logging.info("RESP:\n%s\n\n" % raw_response)
 
@@ -130,8 +131,8 @@ def get_user_info(user_id):
   </soap-env:Envelope>
 """ % user_id
 
-  req = urllib2.Request(url, request_body, headers)
-  response = urllib2.urlopen(req).read()
+  req = urllib.request.Request(url, request_body, headers)
+  response = urllib.request.urlopen(req).read()
   
   # parse the result
   from xml.dom.minidom import parseString
@@ -149,12 +150,12 @@ def get_user_info(user_id):
 def get_user_info_special(ticket):
   # fetch the information from the CAS server
   val_url = CAS_URL + "validate" + \
-     '?service=' + urllib.quote(_get_service_url()) + \
-     '&ticket=' + urllib.quote(ticket)
-  r = urllib.urlopen(val_url).readlines() # returns 2 lines
+     '?service=' + urllib.parse.quote(_get_service_url()) + \
+     '&ticket=' + urllib.parse.quote(ticket)
+  r = urllib.request.urlopen(val_url).readlines() # returns 2 lines
 
   # success
-  if len(r) == 2 and re.match("yes", r[0]) != None:
+  if len(r) == 2 and re.match("yes", r[0]) is not None:
     netid = r[1].strip()
     
     category = get_user_category(netid)
@@ -212,7 +213,7 @@ def send_message(user_id, name, user_info, subject, body):
   else:
     email = "%s@%s" % (user_id, CAS_EMAIL_DOMAIN)
     
-  if user_info.has_key('name'):
+  if 'name' in user_info:
     name = user_info["name"]
   else:
     name = email
@@ -224,7 +225,7 @@ def send_message(user_id, name, user_info, subject, body):
 #
 
 def check_constraint(constraint, user):
-  if not user.info.has_key('category'):
+  if 'category' not in user.info:
     return False
   return constraint['year'] == user.info['category']
 
diff --git a/helios_auth/auth_systems/clever.py b/helios_auth/auth_systems/clever.py
index 498951f8dbf0e3d60f2460c83d580be0b703bb64..48648a0b07a55add763512be3beb457ed08bb298 100644
--- a/helios_auth/auth_systems/clever.py
+++ b/helios_auth/auth_systems/clever.py
@@ -4,12 +4,14 @@ Clever Authentication
 """
 
 import base64
+import urllib.parse
+
 import httplib2
-import json
-import urllib
 from django.conf import settings
 from oauth2client.client import OAuth2WebServerFlow, OAuth2Credentials
 
+from helios_auth import utils
+
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
 
@@ -42,7 +44,7 @@ def get_user_info_after_auth(request):
   # do the POST manually, because OAuth2WebFlow can't do auth header for token exchange
   http = httplib2.Http(".cache")
   auth_header = "Basic %s" % base64.b64encode(settings.CLEVER_CLIENT_ID + ":" + settings.CLEVER_CLIENT_SECRET)
-  resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.urlencode({
+  resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.parse.urlencode({
         "code" : code,
         "grant_type": "authorization_code",
         "redirect_uri": redirect_uri
@@ -51,7 +53,7 @@ def get_user_info_after_auth(request):
         'Content-Type': "application/x-www-form-urlencoded"
       })
 
-  token_response = json.loads(content)
+  token_response = utils.from_json(content)
   access_token = token_response['access_token']
 
   # package the credentials
@@ -62,7 +64,7 @@ def get_user_info_after_auth(request):
   (resp_headers, content) = http.request("https://api.clever.com/me", "GET")
 
   # {"type":"student","data":{"id":"563395179f7408755c0006b7","district":"5633941748c07c0100000aac","type":"student","created":"2015-10-30T16:04:39.262Z","credentials":{"district_password":"eel7Thohd","district_username":"dianes10"},"dob":"1998-11-01T00:00:00.000Z","ell_status":"Y","email":"diane.s@example.org","gender":"F","grade":"9","hispanic_ethnicity":"Y","last_modified":"2015-10-30T16:04:39.274Z","location":{"zip":"11433"},"name":{"first":"Diane","last":"Schmeler","middle":"J"},"race":"Asian","school":"5633950c62fc41c041000005","sis_id":"738733110","state_id":"114327752","student_number":"738733110"},"links":[{"rel":"self","uri":"/me"},{"rel":"canonical","uri":"/v1.1/students/563395179f7408755c0006b7"},{"rel":"district","uri":"/v1.1/districts/5633941748c07c0100000aac"}]}
-  response = json.loads(content)
+  response = utils.from_json(content)
   
   user_id = response['data']['id']
   user_name = "%s %s" % (response['data']['name']['first'], response['data']['name']['last'])
@@ -70,7 +72,7 @@ def get_user_info_after_auth(request):
   user_district = response['data']['district']
   user_grade = response['data'].get('grade', None)
 
-  print content
+  print(content)
   
   # watch out, response also contains email addresses, but not sure whether thsoe are verified or not
   # so for email address we will only look at the id_token
@@ -100,7 +102,7 @@ def send_message(user_id, name, user_info, subject, body):
 #
 
 def check_constraint(constraint, user):
-  if not user.info.has_key('grade'):
+  if 'grade' not in user.info:
     return False
   return constraint['grade'] == user.info['grade']
 
diff --git a/helios_auth/auth_systems/facebook.py b/helios_auth/auth_systems/facebook.py
index 4c50c6594336f60c2999303c57088e6feeb77b4c..4810ec20494fef34aba4e5017d092c88e185c39c 100644
--- a/helios_auth/auth_systems/facebook.py
+++ b/helios_auth/auth_systems/facebook.py
@@ -2,8 +2,6 @@
 Facebook Authentication
 """
 
-import logging
-
 from django.conf import settings
 from django.core.mail import send_mail
 
@@ -12,7 +10,7 @@ API_KEY = settings.FACEBOOK_API_KEY
 API_SECRET = settings.FACEBOOK_API_SECRET
   
 #from facebookclient import Facebook
-import urllib, urllib2, cgi
+import urllib.request, urllib.error, urllib.parse
 
 # some parameters to indicate that status updating is possible
 STATUS_UPDATES = True
@@ -22,21 +20,21 @@ from helios_auth import utils
 
 def facebook_url(url, params):
   if params:
-    return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+    return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
   else:
     return "https://graph.facebook.com%s" % url
 
 def facebook_get(url, params):
   full_url = facebook_url(url,params)
   try:
-    return urllib2.urlopen(full_url).read()
-  except urllib2.HTTPError:
+    return urllib.request.urlopen(full_url).read()
+  except urllib.error.HTTPError:
     from helios_auth.models import AuthenticationExpired
     raise AuthenticationExpired()
 
 def facebook_post(url, params):
   full_url = facebook_url(url, None)
-  return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+  return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
 
 def get_auth_url(request, redirect_url):
   request.session['fb_redirect_uri'] = redirect_url
@@ -69,7 +67,7 @@ def update_status(user_id, user_info, token, message):
       })
 
 def send_message(user_id, user_name, user_info, subject, body):
-  if user_info.has_key('email'):
+  if 'email' in user_info:
     send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False)    
 
 
diff --git a/helios_auth/auth_systems/facebookclient/__init__.py b/helios_auth/auth_systems/facebookclient/__init__.py
index 03264730c9ab284b3500793fc232ad15175a945e..df8fe2233d47ba65e3d093a819b3cbb850f92eaa 100644
--- a/helios_auth/auth_systems/facebookclient/__init__.py
+++ b/helios_auth/auth_systems/facebookclient/__init__.py
@@ -44,16 +44,15 @@ http://undefined.org/python/#simplejson to download it, or do
 apt-get install python-simplejson on a Debian-like system.
 """
 
-import sys
-import time
-import struct
-import urllib
-import urllib2
-import httplib
-import hashlib
 import binascii
-import urlparse
+import hashlib
+import http.client
 import mimetypes
+import struct
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
 
 # try to use simplejson first, otherwise fallback to XML
 RESPONSE_FORMAT = 'JSON'
@@ -96,11 +95,11 @@ try:
         if result.status_code == 200:
             return result.content
         else:
-            raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code))
+            raise urllib.error.URLError("fetch error url=%s, code=%d" % (url, result.status_code))
 
 except ImportError:
     def urlread(url, data=None):
-        res = urllib2.urlopen(url, data=data)
+        res = urllib.request.urlopen(url, data=data)
         return res.read()
 
 __all__ = ['Facebook']
@@ -711,9 +710,9 @@ class PhotosProxy(PhotosProxy):
         args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args))
 
         try:
-            import cStringIO as StringIO
+            import io as StringIO
         except ImportError:
-            import StringIO
+            import io
 
         # check for a filename specified...if the user is passing binary data in
         # image then a filename will be specified
@@ -721,26 +720,26 @@ class PhotosProxy(PhotosProxy):
             try:
                 import Image
             except ImportError:
-                data = StringIO.StringIO(open(image, 'rb').read())
+                data = io.StringIO(open(image, 'rb').read())
             else:
                 img = Image.open(image)
                 if size:
                     img.thumbnail(size, Image.ANTIALIAS)
-                data = StringIO.StringIO()
+                data = io.StringIO()
                 img.save(data, img.format)
         else:
             # there was a filename specified, which indicates that image was not
             # the path to an image file but rather the binary data of a file
-            data = StringIO.StringIO(image)
+            data = io.StringIO(image)
             image = filename
 
-        content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)])
-        urlinfo = urlparse.urlsplit(self._client.facebook_url)
+        content_type, body = self.__encode_multipart_formdata(list(args.items()), [(image, data)])
+        urlinfo = urllib.parse.urlsplit(self._client.facebook_url)
         try:
             content_length = len(body)
             chunk_size = 4096
 
-            h = httplib.HTTPConnection(urlinfo[1])
+            h = http.client.HTTPConnection(urlinfo[1])
             h.putrequest('POST', urlinfo[2])
             h.putheader('Content-Type', content_type)
             h.putheader('Content-Length', str(content_length))
@@ -776,7 +775,7 @@ class PhotosProxy(PhotosProxy):
 
                 try:
                     response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'})
-                except urllib2.URLError:
+                except urllib.error.URLError:
                     raise Exception('Error uploading photo: Facebook returned %s' % (response))
             except ImportError:
                 # could not import from google.appengine.api, so we are not running in GAE
@@ -954,7 +953,7 @@ class Facebook(object):
         """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest."""
         # @author: houyr
         # fix for UnicodeEncodeError
-        hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())]))
+        hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, str) and x.encode("utf-8") or x, isinstance(args[x], str) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())]))
         if secret:
             hasher.update(secret)
         elif self.secret:
@@ -976,7 +975,7 @@ class Facebook(object):
             node.hasAttribute('list') and \
             node.getAttribute('list')=="true":
             return self._parse_response_list(node)
-        elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0:
+        elif len([x for x in node.childNodes if x.nodeType == x.ELEMENT_NODE]) > 0:
             return self._parse_response_dict(node)
         else:
             return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE)
@@ -985,7 +984,7 @@ class Facebook(object):
     def _parse_response_dict(self, node):
         """Parses an XML dictionary response node from Facebook."""
         result = {}
-        for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
+        for item in [x for x in node.childNodes if x.nodeType == x.ELEMENT_NODE]:
             result[item.nodeName] = self._parse_response_item(item)
         if node.nodeType == node.ELEMENT_NODE and node.hasAttributes():
             if node.hasAttribute('id'):
@@ -996,14 +995,14 @@ class Facebook(object):
     def _parse_response_list(self, node):
         """Parses an XML list response node from Facebook."""
         result = []
-        for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes):
+        for item in [x for x in node.childNodes if x.nodeType == x.ELEMENT_NODE]:
             result.append(self._parse_response_item(item))
         return result
 
 
     def _check_error(self, response):
         """Checks if the given Facebook response is an error, and then raises the appropriate exception."""
-        if type(response) is dict and response.has_key('error_code'):
+        if isinstance(response, dict) and 'error_code' in response:
             raise FacebookError(response['error_code'], response['error_msg'], response['request_args'])
 
 
@@ -1012,12 +1011,12 @@ class Facebook(object):
         if args is None:
             args = {}
 
-        for arg in args.items():
-            if type(arg[1]) == list:
+        for arg in list(args.items()):
+            if isinstance(arg[1], list):
                 args[arg[0]] = ','.join(str(a) for a in arg[1])
-            elif type(arg[1]) == unicode:
+            elif isinstance(arg[1], str):
                 args[arg[0]] = arg[1].encode("UTF-8")
-            elif type(arg[1]) == bool:
+            elif isinstance(arg[1], bool):
                 args[arg[0]] = str(arg[1]).lower()
 
         args['method'] = method
@@ -1087,8 +1086,8 @@ class Facebook(object):
         A unicode aware version of urllib.urlencode.
         """
         if isinstance(params, dict):
-            params = params.items()
-        return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v)
+            params = list(params.items())
+        return urllib.parse.urlencode([(k, isinstance(v, str) and v.encode('utf-8') or v)
                           for k, v in params])
 
 
@@ -1100,7 +1099,7 @@ class Facebook(object):
             return self
 
         # __init__ hard-codes into en_US
-        if args is not None and not args.has_key('locale'):
+        if args is not None and 'locale' not in args:
             args['locale'] = self.locale
 
         # @author: houyr
@@ -1108,8 +1107,8 @@ class Facebook(object):
         post_data = self.unicode_urlencode(self._build_post_args(method, args))
 
         if self.proxy:
-            proxy_handler = urllib2.ProxyHandler(self.proxy)
-            opener = urllib2.build_opener(proxy_handler)
+            proxy_handler = urllib.request.ProxyHandler(self.proxy)
+            opener = urllib.request.build_opener(proxy_handler)
             if secure:
                 response = opener.open(self.facebook_secure_url, post_data).read()
             else:
@@ -1130,7 +1129,7 @@ class Facebook(object):
         Named arguments are passed as GET query string parameters.
 
         """
-        return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args))
+        return 'http://www.facebook.com/%s.php?%s' % (page, urllib.parse.urlencode(args))
 
 
     def get_app_url(self, path=''):
@@ -1255,7 +1254,7 @@ class Facebook(object):
 
                 try:
                     self.auth.getSession()
-                except FacebookError, e:
+                except FacebookError as e:
                     self.auth_token = None
                     return False
 
@@ -1349,7 +1348,7 @@ class Facebook(object):
         if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout:
             return None
 
-        args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)])
+        args = dict([(key[len(prefix + '_'):], value) for key, value in list(args.items()) if key.startswith(prefix)])
 
         hash = self._hash_args(args)
 
@@ -1406,24 +1405,24 @@ if __name__ == '__main__':
     facebook.login()
 
     # Login to the window, then press enter
-    print 'After logging in, press enter...'
-    raw_input()
+    print('After logging in, press enter...')
+    input()
 
     facebook.auth.getSession()
-    print 'Session Key:   ', facebook.session_key
-    print 'Your UID:      ', facebook.uid
+    print('Session Key:   ', facebook.session_key)
+    print('Your UID:      ', facebook.uid)
 
     info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0]
 
-    print 'Your Name:     ', info['name']
-    print 'Your Birthday: ', info['birthday']
-    print 'Your Gender:   ', info['sex']
+    print('Your Name:     ', info['name'])
+    print('Your Birthday: ', info['birthday'])
+    print('Your Gender:   ', info['sex'])
 
     friends = facebook.friends.get()
     friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status'])
 
     for friend in friends:
-        print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status']
+        print(friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'])
 
     arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']])
 
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
index 68b1b27c37d19e1372369837006947b4f60ef7c5..48f720be150401e5a063e7508aada478951fd514 100644
--- a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
+++ b/helios_auth/auth_systems/facebookclient/djangofb/__init__.py
@@ -64,7 +64,7 @@ def require_login(next=None, internal=None):
     """
     def decorator(view):
         def newview(request, *args, **kwargs):
-            next = newview.next
+            next = newview.__next__
             internal = newview.internal
 
             try:
@@ -127,7 +127,7 @@ def require_add(next=None, internal=None, on_install=None):
     """
     def decorator(view):
         def newview(request, *args, **kwargs):
-            next = newview.next
+            next = newview.__next__
             internal = newview.internal
 
             try:
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py b/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
index 6f954397308f7af525d9fa600fb29e8cf6902c33..82805a537f3033eb9f7b7597cc2ca7b17fddb82e 100644
--- a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
+++ b/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py
@@ -1,6 +1,6 @@
 def messages(request):
     """Returns messages similar to ``django.core.context_processors.auth``."""
     if hasattr(request, 'facebook') and request.facebook.uid is not None:
-        from models import Message
+        from .models import Message
         messages = Message.objects.get_and_delete_all(uid=request.facebook.uid)
     return {'messages': messages}
\ No newline at end of file
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
index f75d8d258360fc43e12b2343182d89f14a01ef8c..d6084742bb17200bb3a452905e94f734f4c5b6be 100644
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
+++ b/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py
@@ -1,6 +1,6 @@
 from django.conf.urls import url
 
-from views import canvas
+from .views import canvas
 
 urlpatterns = [
     url(r'^$', canvas),
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
index 609314fe01b3bf546984841b9dded39756bfa0cb..1cd91f831ca38cbd27f6823e9f979bd83d5df729 100644
--- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
+++ b/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py
@@ -9,7 +9,7 @@ from django.http import HttpResponse
 import facebook.djangofb as facebook
 
 # The User model defined in models.py
-from models import User
+from .models import User
 
 # We'll require login for our canvas page. This
 # isn't necessarily a good idea, as we might want
diff --git a/helios_auth/auth_systems/facebookclient/djangofb/models.py b/helios_auth/auth_systems/facebookclient/djangofb/models.py
index b5d2c62221e9926f7ab4b57cb95fb71ab22be2da..07604f40d2525e1837c099442af2de9f8479e1ae 100644
--- a/helios_auth/auth_systems/facebookclient/djangofb/models.py
+++ b/helios_auth/auth_systems/facebookclient/djangofb/models.py
@@ -30,7 +30,7 @@ class Message(models.Model):
         return self.get_status_display().lower()
 
     def as_fbml(self):
-        return mark_safe(u'<fb:%s message="%s" />' % (
+        return mark_safe('<fb:%s message="%s" />' % (
             self._fb_tag(),
             escape(self.message),
         ))
diff --git a/helios_auth/auth_systems/google.py b/helios_auth/auth_systems/google.py
index 03419915600fe58636ce9e2098a0219075fa804d..b713404de7d1bb9b527294f0c2baa571679c36f3 100644
--- a/helios_auth/auth_systems/google.py
+++ b/helios_auth/auth_systems/google.py
@@ -4,11 +4,12 @@ Google Authentication
 """
 
 import httplib2
-import json
 from django.conf import settings
 from django.core.mail import send_mail
 from oauth2client.client import OAuth2WebServerFlow
 
+from helios_auth import utils
+
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
 
@@ -30,7 +31,7 @@ def get_auth_url(request, redirect_url):
 def get_user_info_after_auth(request):
   flow = get_flow(request.session['google-redirect-url'])
 
-  if not request.GET.has_key('code'):
+  if 'code' not in request.GET:
     return None
   
   code = request.GET['code']
@@ -48,7 +49,7 @@ def get_user_info_after_auth(request):
   http = credentials.authorize(http)
   (resp_headers, content) = http.request("https://people.googleapis.com/v1/people/me?personFields=names", "GET")
 
-  response = json.loads(content)
+  response = utils.from_json(content)
 
   name = response['names'][0]['displayName']
   
diff --git a/helios_auth/auth_systems/linkedin.py b/helios_auth/auth_systems/linkedin.py
index 696eda9836f0c141360d24f00739d8ed66fca95e..e75e0786d46a508f9846bd6b9d1491be1bdbcbc5 100644
--- a/helios_auth/auth_systems/linkedin.py
+++ b/helios_auth/auth_systems/linkedin.py
@@ -2,18 +2,12 @@
 LinkedIn Authentication
 """
 
-from oauthclient import client
-
-from django.urls import reverse
-from django.http import HttpResponseRedirect
-
-from helios_auth import utils
-
 from xml.etree import ElementTree
 
-import logging
-
 from django.conf import settings
+
+from .oauthclient import client
+
 API_KEY = settings.LINKEDIN_API_KEY
 API_SECRET = settings.LINKEDIN_API_SECRET
 
diff --git a/helios_auth/auth_systems/live.py b/helios_auth/auth_systems/live.py
index 9f34a2783198003f09a74abb1671d9630cf3895a..348d02ff34a22d685b3d6dbe46aa5885ad162b4d 100644
--- a/helios_auth/auth_systems/live.py
+++ b/helios_auth/auth_systems/live.py
@@ -5,14 +5,14 @@ so much like Facebook
 # NOT WORKING YET because Windows Live documentation and status is unclear. Is it in beta? I think it is.
 """
 
-import logging
+import urllib.parse
+import urllib.request
 
 from django.conf import settings
+
 APP_ID = settings.LIVE_APP_ID
 APP_SECRET = settings.LIVE_APP_SECRET
   
-import urllib, urllib2, cgi
-
 # some parameters to indicate that status updating is possible
 STATUS_UPDATES = False
 # STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status"
@@ -21,17 +21,17 @@ from helios_auth import utils
 
 def live_url(url, params):
   if params:
-    return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params))
+    return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params))
   else:
     return "https://graph.facebook.com%s" % url
 
 def live_get(url, params):
   full_url = live_url(url,params)
-  return urllib2.urlopen(full_url).read()
+  return urllib.request.urlopen(full_url).read()
 
 def live_post(url, params):
   full_url = live_url(url, None)
-  return urllib2.urlopen(full_url, urllib.urlencode(params)).read()
+  return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read()
 
 def get_auth_url(request, redirect_url):
   request.session['live_redirect_uri'] = redirect_url
@@ -41,16 +41,16 @@ def get_auth_url(request, redirect_url):
       'scope': 'publish_stream'})
     
 def get_user_info_after_auth(request):
-  args = facebook_get('/oauth/access_token', {
+  args = live_get('/oauth/access_token', {
       'client_id' : APP_ID,
       'redirect_uri' : request.session['fb_redirect_uri'],
-      'client_secret' : API_SECRET,
+      'client_secret' : APP_SECRET,
       'code' : request.GET['code']
       })
 
-  access_token = cgi.parse_qs(args)['access_token'][0]
+  access_token = urllib.parse.parse_qs(args)['access_token'][0]
 
-  info = utils.from_json(facebook_get('/me', {'access_token':access_token}))
+  info = utils.from_json(live_get('/me', {'access_token':access_token}))
 
   return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'info': info, 'token': {'access_token': access_token}}
     
@@ -58,7 +58,7 @@ def update_status(user_id, user_info, token, message):
   """
   post a message to the auth system's update stream, e.g. twitter stream
   """
-  result = facebook_post('/me/feed', {
+  result = live_post('/me/feed', {
       'access_token': token['access_token'],
       'message': message
       })
diff --git a/helios_auth/auth_systems/oauthclient/client.py b/helios_auth/auth_systems/oauthclient/client.py
index c9e6b829bb946694130cabcc6b336939864bcb3a..4980535e45ab58942cc632c00ecc50c0dd33d578 100644
--- a/helios_auth/auth_systems/oauthclient/client.py
+++ b/helios_auth/auth_systems/oauthclient/client.py
@@ -7,12 +7,11 @@ Used the SampleClient from the OAUTH.org example python client as basis.
 props to leahculver for making a very hard to use but in the end usable oauth lib.
 
 '''
-import httplib
-import urllib, urllib2
-import time
+import urllib.request
 import webbrowser
-import oauth as oauth
-from urlparse import urlparse
+
+from . import oauth as oauth
+
 
 class LoginOAuthClient(oauth.OAuthClient):
 
@@ -36,31 +35,35 @@ class LoginOAuthClient(oauth.OAuthClient):
 
         self.sha1_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
         self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
-        if ((oauth_token != None) and (oauth_token_secret!=None)):
+        if (oauth_token is not None) and (oauth_token_secret is not None):
             self.token = oauth.OAuthConsumer(oauth_token, oauth_token_secret)
         else:
             self.token = None
 
-    def oauth_request(self,url, args = {}, method=None):
-        if (method==None):
-            if args=={}:
+    def oauth_request(self, url, args=None, method=None):
+        if args is None:
+            args = {}
+        if method is None:
+            if args == {}:
                 method = "GET"
             else:
                 method = "POST"
         req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args)
         req.sign_request(self.sha1_method, self.consumer,self.token)
-        if (method=="GET"):
+        if method== "GET":
             return self.http_wrapper(req.to_url())
-        elif (method == "POST"):
+        elif method == "POST":
             return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata())
 
     #this is barely working. (i think. mostly it is that everyone else is using httplib) 
-    def http_wrapper(self, url, postdata={}): 
+    def http_wrapper(self, url, postdata=None):
+        if postdata is None:
+            postdata = {}
         try:
-            if (postdata != {}): 
-                f = urllib.urlopen(url, postdata) 
+            if postdata != {}:
+                f = urllib.request.urlopen(url, postdata) 
             else: 
-                f = urllib.urlopen(url) 
+                f = urllib.request.urlopen(url) 
             response = f.read()
         except:
             import traceback
@@ -133,26 +136,26 @@ if __name__ == '__main__':
     consumer_key = ''
     consumer_secret = ''
     while not consumer_key:
-        consumer_key = raw_input('Please enter consumer key: ')
+        consumer_key = input('Please enter consumer key: ')
     while not consumer_secret:
-        consumer_secret = raw_input('Please enter consumer secret: ')
+        consumer_secret = input('Please enter consumer secret: ')
     auth_client = LoginOAuthClient(consumer_key,consumer_secret)
     tok = auth_client.get_request_token()
     token = tok['oauth_token']
     token_secret = tok['oauth_token_secret']
     url = auth_client.get_authorize_url(token) 
     webbrowser.open(url)
-    print "Visit this URL to authorize your app: " + url
-    response_token = raw_input('What is the oauth_token from twitter: ')
+    print("Visit this URL to authorize your app: " + url)
+    response_token = input('What is the oauth_token from twitter: ')
     response_client = LoginOAuthClient(consumer_key, consumer_secret,token, token_secret, server_params={})
     tok = response_client.get_access_token()
-    print "Making signed request"
+    print("Making signed request")
     #verify user access
     content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST')
     #make an update
     #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST')
-    print content
+    print(content)
    
-    print 'Done.'
+    print('Done.')
 
 
diff --git a/helios_auth/auth_systems/oauthclient/oauth/__init__.py b/helios_auth/auth_systems/oauthclient/oauth/__init__.py
index baf543ed4a8db09d92ee91b925d18bc6f5d09f93..f318a8af4421f92c9a5276d619fd8ef52dbe9644 100755
--- a/helios_auth/auth_systems/oauthclient/oauth/__init__.py
+++ b/helios_auth/auth_systems/oauthclient/oauth/__init__.py
@@ -1,10 +1,8 @@
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
 import binascii
+import hmac
+import random
+import time
+import urllib.parse
 
 VERSION = '1.0' # Hi Blaine!
 HTTP_METHOD = 'GET'
@@ -22,7 +20,7 @@ def build_authenticate_header(realm=''):
 # url escape
 def escape(s):
     # escape '/' too
-    return urllib.quote(s, safe='~')
+    return urllib.parse.quote(s, safe='~')
 
 # util function: current timestamp
 # seconds since epoch (UTC)
@@ -60,12 +58,12 @@ class OAuthToken(object):
         self.secret = secret
 
     def to_string(self):
-        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+        return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
 
     # return a token from something like:
     # oauth_token_secret=digg&oauth_token=digg
     def from_string(s):
-        params = cgi.parse_qs(s, keep_blank_values=False)
+        params = urllib.parse.parse_qs(s, keep_blank_values=False)
         key = params['oauth_token'][0]
         secret = params['oauth_token_secret'][0]
         return OAuthToken(key, secret)
@@ -112,7 +110,7 @@ class OAuthRequest(object):
     # get any non-oauth parameters
     def get_nonoauth_parameters(self):
         parameters = {}
-        for k, v in self.parameters.iteritems():
+        for k, v in self.parameters.items():
             # ignore oauth parameters
             if k.find('oauth_') < 0:
                 parameters[k] = v
@@ -123,14 +121,14 @@ class OAuthRequest(object):
         auth_header = 'OAuth realm="%s"' % realm
         # add the oauth parameters
         if self.parameters:
-            for k, v in self.parameters.iteritems():
+            for k, v in self.parameters.items():
                 if k[:6] == 'oauth_':
                     auth_header += ', %s="%s"' % (k, escape(str(v)))
         return {'Authorization': auth_header}
 
     # serialize as post data for a POST request
     def to_postdata(self):
-        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()])
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items()])
 
     # serialize as a url for a GET request
     def to_url(self):
@@ -144,7 +142,7 @@ class OAuthRequest(object):
             del params['oauth_signature']
         except:
             pass
-        key_values = params.items()
+        key_values = list(params.items())
         # sort lexicographically, first after key, then after value
         key_values.sort()
         # combine key value pairs in string and escape
@@ -156,7 +154,7 @@ class OAuthRequest(object):
 
     # parses the url and rebuilds it to be scheme://host/path
     def get_normalized_http_url(self):
-        parts = urlparse.urlparse(self.http_url)
+        parts = urllib.parse.urlparse(self.http_url)
         url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
         return url_string
         
@@ -194,7 +192,7 @@ class OAuthRequest(object):
             parameters.update(query_params)
 
         # URL parameters
-        param_str = urlparse.urlparse(http_url)[4] # query
+        param_str = urllib.parse.urlparse(http_url)[4] # query
         url_params = OAuthRequest._split_url_string(param_str)
         parameters.update(url_params)
 
@@ -249,15 +247,15 @@ class OAuthRequest(object):
             # split key-value
             param_parts = param.split('=', 1)
             # remove quotes and unescape the value
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+            params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
         return params
     _split_header = staticmethod(_split_header)
     
     # util function: turn url string into parameters, has to do some unescaping
     def _split_url_string(param_str):
-        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
+        parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.items():
+            parameters[k] = urllib.parse.unquote(v[0])
         return parameters
     _split_url_string = staticmethod(_split_url_string)
 
@@ -273,7 +271,7 @@ class OAuthServer(object):
         self.signature_methods = signature_methods or {}
 
     def set_data_store(self, oauth_data_store):
-        self.data_store = data_store
+        self.data_store = oauth_data_store
 
     def get_data_store(self):
         return self.data_store
@@ -351,7 +349,7 @@ class OAuthServer(object):
             # get the signature method object
             signature_method = self.signature_methods[signature_method]
         except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
+            signature_method_names = ', '.join(list(self.signature_methods.keys()))
             raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
 
         return signature_method
@@ -499,11 +497,11 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
 
         # hmac object
         try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
+            from Crypto.Hash import SHA1
+            hashed = hmac.new(key, raw, SHA1)
         except:
-            import sha # deprecated
-            hashed = hmac.new(key, raw, sha)
+            import hashlib
+            hashed = hmac.new(key, raw, hashlib.sha1)
 
         # calculate the digest base 64
         return binascii.b2a_base64(hashed.digest())[:-1]
diff --git a/helios_auth/auth_systems/openid/util.py b/helios_auth/auth_systems/openid/util.py
index 1ed33f3be0f82de0f2f9d545a79ccf8b972bd878..2a2e12a429b0206f6700acbb929b5e01ffdfe11f 100644
--- a/helios_auth/auth_systems/openid/util.py
+++ b/helios_auth/auth_systems/openid/util.py
@@ -3,20 +3,15 @@
 Utility code for the Django example consumer and server.
 """
 
-from urlparse import urljoin
+from urllib.parse import urljoin
 
-from django.db import connection
-from django.template.context import RequestContext
-from django.template import loader
-from django import http
+from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
+from django.db import connection
 from django.urls import reverse as reverseURL
-
-from django.conf import settings
-
-from openid.store.filestore import FileOpenIDStore
 from openid.store import sqlstore
-from openid.yadis.constants import YADIS_CONTENT_TYPE
+from openid.store.filestore import FileOpenIDStore
+
 
 def getOpenIDStore(filestore_path, table_prefix):
     """
@@ -69,14 +64,13 @@ def getOpenIDStore(filestore_path, table_prefix):
         s = types[db_engine](connection.connection,
                                             **tablenames)
     except KeyError:
-        raise ImproperlyConfigured, \
-              "Database engine %s not supported by OpenID library" % \
-              (db_engine,)
+        raise ImproperlyConfigured("Database engine %s not supported by OpenID library" % \
+              (db_engine,))
 
     try:
         s.createTables()
-    except (SystemExit, KeyboardInterrupt, MemoryError), e:
-        raise
+    except (SystemExit, KeyboardInterrupt, MemoryError) as e:
+        raise e
     except:
         # XXX This is not the Right Way to do this, but because the
         # underlying database implementation might differ in behavior
@@ -138,5 +132,5 @@ def normalDict(request_data):
     values are lists, because in OpenID, each key in the query arg set
     can have at most one value.
     """
-    return dict((k, v[0]) for k, v in request_data.iteritems())
+    return dict((k, v[0]) for k, v in request_data.items())
 
diff --git a/helios_auth/auth_systems/openid/view_helpers.py b/helios_auth/auth_systems/openid/view_helpers.py
index 06eef8a287bbbf0d5a05d32d8ed0e13939761eac..de79b451a037800f974e33535e52569af754c1c6 100644
--- a/helios_auth/auth_systems/openid/view_helpers.py
+++ b/helios_auth/auth_systems/openid/view_helpers.py
@@ -1,14 +1,8 @@
-
-from django import http
-from django.http import HttpResponseRedirect
-
 from openid.consumer import consumer
 from openid.consumer.discover import DiscoveryFailure
 from openid.extensions import ax, pape, sreg
-from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
-from openid.server.trustroot import RP_RETURN_TO_URL_TYPE
 
-import util
+from . import util
 
 PAPE_POLICIES = [
     'AUTH_PHISHING_RESISTANT',
@@ -56,16 +50,12 @@ def start_openid(session, openid_url, trust_root, return_to):
 
     # Start OpenID authentication.
     c = get_consumer(session)
-    error = None
 
     try:
         auth_request = c.begin(openid_url)
-    except DiscoveryFailure, e:
+    except DiscoveryFailure as e:
         # Some other protocol-level failure occurred.
-        error = "OpenID discovery error: %s" % (str(e),)
-
-    if error:
-        raise Exception("error in openid")
+        raise Exception("error in openid: OpenID discovery error") from e
 
     # Add Simple Registration request information.  Some fields
     # are optional, some are required.  It's possible that the
@@ -80,7 +70,7 @@ def start_openid(session, openid_url, trust_root, return_to):
     # XXX - uses myOpenID-compatible schema values, which are
     # not those listed at axschema.org.
 
-    for k, v in AX_REQUIRED_FIELDS.iteritems():
+    for k, v in AX_REQUIRED_FIELDS.items():
         ax_request.add(ax.AttrInfo(v, required=True))
 
     auth_request.addExtension(ax_request)
@@ -123,12 +113,12 @@ def finish_openid(session, request_args, return_to):
 
             ax_response = ax.FetchResponse.fromSuccessResponse(response)
             if ax_response:
-                for k, v in AX_REQUIRED_FIELDS.iteritems():
+                for k, v in AX_REQUIRED_FIELDS.items():
                     """
                     the values are the URIs, they are the key into the data
                     the key is the shortname
                     """
-                    if ax_response.data.has_key(v):
+                    if v in ax_response.data:
                         ax_items[k] = ax_response.get(v)
 
         # Map different consumer status codes to template contexts.
@@ -141,7 +131,7 @@ def finish_openid(session, request_args, return_to):
 
             consumer.SUCCESS:
             {'url': response.getDisplayIdentifier(),
-             'sreg': sreg_response and sreg_response.items(),
+             'sreg': sreg_response and list(sreg_response.items()),
              'ax': ax_items}
             }
 
diff --git a/helios_auth/auth_systems/password.py b/helios_auth/auth_systems/password.py
index aadb03fb05e52d9bbe4e9b2d5e8e02933b89590f..36f42ecffcc109f283d157e1ffe2a37fadb8b706 100644
--- a/helios_auth/auth_systems/password.py
+++ b/helios_auth/auth_systems/password.py
@@ -52,7 +52,7 @@ def password_login_view(request):
     # set this in case we came here straight from the multi-login chooser
     # and thus did not have a chance to hit the "start/password" URL
     request.session['auth_system_name'] = 'password'
-    if request.POST.has_key('return_url'):
+    if 'return_url' in request.POST:
       request.session['auth_return_url'] = request.POST.get('return_url')
 
     if form.is_valid():
diff --git a/helios_auth/auth_systems/twitter.py b/helios_auth/auth_systems/twitter.py
index 8739d607ebcb1d85b0b539c8f85eb34cceaa4ce8..541ac4df7cd874259a71be3566246fa3b2a48b57 100644
--- a/helios_auth/auth_systems/twitter.py
+++ b/helios_auth/auth_systems/twitter.py
@@ -2,7 +2,7 @@
 Twitter Authentication
 """
 
-from oauthclient import client
+from .oauthclient import client
 
 from django.conf.urls import url
 from django.urls import reverse
diff --git a/helios_auth/auth_systems/yahoo.py b/helios_auth/auth_systems/yahoo.py
index 5131a19be520301ab4ea6643e88d97015d186d0f..ccfdc12ffd8850dc17feb54bee08b4fa784a5c1e 100644
--- a/helios_auth/auth_systems/yahoo.py
+++ b/helios_auth/auth_systems/yahoo.py
@@ -6,7 +6,7 @@ Yahoo Authentication
 from django.conf import settings
 from django.core.mail import send_mail
 
-from openid import view_helpers
+from .openid import view_helpers
 
 # some parameters to indicate that status updating is not possible
 STATUS_UPDATES = False
diff --git a/helios_auth/jsonfield.py b/helios_auth/jsonfield.py
index 34cecf7894487c1141388abe13d5a0934a4ab9dc..ab66d5d5d95f5e48860e36f88a4f70124cfee558 100644
--- a/helios_auth/jsonfield.py
+++ b/helios_auth/jsonfield.py
@@ -5,10 +5,12 @@ http://www.djangosnippets.org/snippets/377/
 """
 
 import json
-from django.core.exceptions import ValidationError
+
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 
+from . import utils
+
 
 class JSONField(models.TextField):
     """
@@ -37,14 +39,10 @@ class JSONField(models.TextField):
 
     # noinspection PyUnusedLocal
     def from_db_value(self, value, *args, **kwargs):
-        if value == "" or value is None:
+        parsed_value = utils.from_json(value)
+        if parsed_value is None:
             return None
 
-        try:
-            parsed_value = json.loads(value)
-        except Exception as e:
-            raise ValidationError("Received value is not JSON", e)
-
         if self.json_type and parsed_value:
             parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params)
 
@@ -55,7 +53,7 @@ class JSONField(models.TextField):
 
     def get_prep_value(self, value):
         """Convert our JSON object to a string before we save"""
-        if isinstance(value, basestring):
+        if isinstance(value, str):
             return value
 
         if value is None:
diff --git a/helios_auth/migrations/0001_initial.py b/helios_auth/migrations/0001_initial.py
index fc54ff2aa58d29872fdd0453e87ffb9123974af8..002068a63be3355f5eb285660f1b0eb5b7514816 100644
--- a/helios_auth/migrations/0001_initial.py
+++ b/helios_auth/migrations/0001_initial.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals
 
 from django.db import models, migrations
+
 import helios_auth.jsonfield
 
 
diff --git a/helios_auth/models.py b/helios_auth/models.py
index fb050d225808599e95459e057f7ff0489e5c1ecf..d4e3a3282ce220688b109da22fa018c667817125 100644
--- a/helios_auth/models.py
+++ b/helios_auth/models.py
@@ -8,8 +8,8 @@ Ben Adida
 """
 from django.db import models
 
-from auth_systems import AUTH_SYSTEMS
-from jsonfield import JSONField
+from .auth_systems import AUTH_SYSTEMS
+from .jsonfield import JSONField
 
 
 # an exception to catch when a user is no longer authenticated
@@ -53,7 +53,7 @@ class User(models.Model):
     
     if not created_p:
       # special case the password: don't replace it if it exists
-      if obj.info.has_key('password'):
+      if 'password' in obj.info:
         info['password'] = obj.info['password']
 
       obj.info = info
@@ -64,7 +64,7 @@ class User(models.Model):
     return obj
     
   def can_update_status(self):
-    if not AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type not in AUTH_SYSTEMS:
       return False
 
     return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES
@@ -74,7 +74,7 @@ class User(models.Model):
     Certain auth systems can choose to limit election creation
     to certain users. 
     """
-    if not AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type not in AUTH_SYSTEMS:
       return False
     
     return AUTH_SYSTEMS[self.user_type].can_create_election(self.user_id, self.info)
@@ -86,16 +86,16 @@ class User(models.Model):
     return AUTH_SYSTEMS[self.user_type].STATUS_UPDATE_WORDING_TEMPLATE
 
   def update_status(self, status):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       AUTH_SYSTEMS[self.user_type].update_status(self.user_id, self.info, self.token, status)
       
   def send_message(self, subject, body):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       subject = subject.split("\n")[0]
       AUTH_SYSTEMS[self.user_type].send_message(self.user_id, self.name, self.info, subject, body)
 
   def send_notification(self, message):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       if hasattr(AUTH_SYSTEMS[self.user_type], 'send_notification'):
         AUTH_SYSTEMS[self.user_type].send_notification(self.user_id, self.info, message)
   
@@ -110,7 +110,7 @@ class User(models.Model):
       return False
       
     # no constraint? Then eligible!
-    if not eligibility_case.has_key('constraint'):
+    if 'constraint' not in eligibility_case:
       return True
     
     # from here on we know we match the auth system, but do we match one of the constraints?  
@@ -141,14 +141,14 @@ class User(models.Model):
     if self.name:
       return self.name
 
-    if self.info.has_key('name'):
+    if 'name' in self.info:
       return self.info['name']
 
     return self.user_id
   
   @property
   def public_url(self):
-    if AUTH_SYSTEMS.has_key(self.user_type):
+    if self.user_type in AUTH_SYSTEMS:
       if hasattr(AUTH_SYSTEMS[self.user_type], 'public_url'):
         return AUTH_SYSTEMS[self.user_type].public_url(self.user_id)
 
diff --git a/helios_auth/security/__init__.py b/helios_auth/security/__init__.py
index d3c5ac184a160d6790b1f04e8584341919dd3ef5..1fa30e1cf3d5edef11b7a445f4527235f0f567d8 100644
--- a/helios_auth/security/__init__.py
+++ b/helios_auth/security/__init__.py
@@ -12,7 +12,7 @@ from django.http import HttpResponseRedirect
 # nicely update the wrapper function
 from functools import update_wrapper
 
-import oauth
+from . import oauth
 from helios_auth.models import User
 
 FIELDS_TO_SAVE = 'FIELDS_TO_SAVE'
@@ -93,10 +93,10 @@ def get_user(request):
   # request.session.set_expiry(settings.SESSION_COOKIE_AGE)
   
   # set up CSRF protection if needed
-  if not request.session.has_key('csrf_token') or (type(request.session['csrf_token']) != str and type(request.session['csrf_token']) != unicode):
+  if 'csrf_token' not in request.session or not isinstance(request.session['csrf_token'], str):
     request.session['csrf_token'] = str(uuid.uuid4())
 
-  if request.session.has_key('user'):
+  if 'user' in request.session:
     user = request.session['user']
 
     # find the user
@@ -109,7 +109,7 @@ def check_csrf(request):
   if request.method != "POST":
     return HttpResponseNotAllowed("only a POST for this URL")
     
-  if (not request.POST.has_key('csrf_token')) or (request.POST['csrf_token'] != request.session['csrf_token']):
+  if ('csrf_token' not in request.POST) or (request.POST['csrf_token'] != request.session['csrf_token']):
     raise Exception("A CSRF problem was detected")
 
 def save_in_session_across_logouts(request, field_name, field_value):
diff --git a/helios_auth/security/oauth.py b/helios_auth/security/oauth.py
index 71676c89f039eebadbb1129081e8e96d2f638ee7..568272bd835eaae3a3f9238b821fab8b870222e8 100644
--- a/helios_auth/security/oauth.py
+++ b/helios_auth/security/oauth.py
@@ -6,14 +6,12 @@ Hacked a bit by Ben Adida (ben@adida.net) so that:
 - access tokens are looked up with an extra param of consumer
 """
 
-import urllib
-import time
-import random
-import urlparse
-import hmac
 import base64
+import hmac
 import logging
-import hashlib
+import random
+import time
+import urllib.parse
 
 VERSION = '1.0' # Hi Blaine!
 HTTP_METHOD = 'GET'
@@ -31,7 +29,7 @@ def build_authenticate_header(realm=''):
 # url escape
 def escape(s):
     # escape '/' too
-    return urllib.quote(s, safe='~')
+    return urllib.parse.quote(s, safe='~')
 
 # util function: current timestamp
 # seconds since epoch (UTC)
@@ -69,13 +67,13 @@ class OAuthToken(object):
         self.secret = secret
 
     def to_string(self):
-        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+        return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
 
     # return a token from something like:
     # oauth_token_secret=digg&oauth_token=digg
     @staticmethod
     def from_string(s):
-        params = urlparse.parse_qs(s, keep_blank_values=False)
+        params = urllib.parse.parse_qs(s, keep_blank_values=False)
         key = params['oauth_token'][0]
         secret = params['oauth_token_secret'][0]
         return OAuthToken(key, secret)
@@ -124,7 +122,7 @@ class OAuthRequest(object):
     # get any non-oauth parameters
     def get_nonoauth_parameters(self):
         parameters = {}
-        for k, v in self.parameters.iteritems():
+        for k, v in self.parameters.items():
             # ignore oauth parameters
             if k.find('oauth_') < 0:
                 parameters[k] = v
@@ -135,7 +133,7 @@ class OAuthRequest(object):
         auth_header = 'OAuth realm="%s"' % realm
         # add the oauth parameters
         if self.parameters:
-            for k, v in self.parameters.iteritems():
+            for k, v in self.parameters.items():
               # only if it's a standard OAUTH param (Ben)
               if k in self.OAUTH_PARAMS:
                 auth_header += ', %s="%s"' % (k, escape(str(v)))
@@ -143,7 +141,7 @@ class OAuthRequest(object):
 
     # serialize as post data for a POST request
     def to_postdata(self):
-        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items())
 
     # serialize as a url for a GET request
     def to_url(self):
@@ -157,7 +155,7 @@ class OAuthRequest(object):
             del params['oauth_signature']
         except:
             pass
-        key_values = params.items()
+        key_values = list(params.items())
         # sort lexicographically, first after key, then after value
         key_values.sort()
         # combine key value pairs in string and escape
@@ -169,7 +167,7 @@ class OAuthRequest(object):
 
     # parses the url and rebuilds it to be scheme://host/path
     def get_normalized_http_url(self):
-        parts = urlparse.urlparse(self.http_url)
+        parts = urllib.parse.urlparse(self.http_url)
         url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
         return url_string
 
@@ -208,7 +206,7 @@ class OAuthRequest(object):
             parameters.update(query_params)
 
         # URL parameters
-        param_str = urlparse.urlparse(http_url)[4] # query
+        param_str = urllib.parse.urlparse(http_url)[4] # query
         url_params = OAuthRequest._split_url_string(param_str)
         parameters.update(url_params)
 
@@ -263,15 +261,15 @@ class OAuthRequest(object):
             # split key-value
             param_parts = param.split('=', 1)
             # remove quotes and unescape the value
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+            params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
         return params
 
     # util function: turn url string into parameters, has to do some unescaping
     @staticmethod
     def _split_url_string(param_str):
-        parameters = urlparse.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
+        parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.items():
+            parameters[k] = urllib.parse.unquote(v[0])
         return parameters
 
 # OAuthServer is a worker to check a requests validity against a data store
@@ -364,7 +362,7 @@ class OAuthServer(object):
             # get the signature method object
             signature_method = self.signature_methods[signature_method]
         except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
+            signature_method_names = ', '.join(list(self.signature_methods.keys()))
             raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
 
         return signature_method
@@ -513,7 +511,12 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
         key, raw = self.build_signature_base_string(oauth_request, consumer, token)
 
         # hmac object
-        hashed = hmac.new(key, raw, hashlib.sha1)
+        try:
+            from Crypto.Hash import SHA1
+            hashed = hmac.new(key, raw, SHA1)
+        except:
+            import hashlib
+            hashed = hmac.new(key, raw, hashlib.sha1)
 
         # calculate the digest base 64
         return base64.b64encode(hashed.digest())
diff --git a/helios_auth/tests.py b/helios_auth/tests.py
index de21e3b09c600084194a47bd5cd0f73522a74daa..934933258f49fb8a64752fc241340a1ad4489789 100644
--- a/helios_auth/tests.py
+++ b/helios_auth/tests.py
@@ -3,16 +3,15 @@ Unit Tests for Auth Systems
 """
 
 import unittest
-import models
 
+from django.core import mail
 from django.db import IntegrityError, transaction
-
-from django.test.client import Client
 from django.test import TestCase
+from django.urls import reverse
 
-from django.core import mail
+from . import models, views
+from .auth_systems import AUTH_SYSTEMS, password as password_views
 
-from auth_systems import AUTH_SYSTEMS
 
 class UserModelTests(unittest.TestCase):
 
@@ -23,7 +22,7 @@ class UserModelTests(unittest.TestCase):
         """
         there should not be two users with the same user_type and user_id
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'})
             
             def double_insert():
@@ -36,22 +35,22 @@ class UserModelTests(unittest.TestCase):
         """
         shouldn't create two users, and should reset the password
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name':'Foo Bar'})
 
             def double_update_or_create():
                 new_name = 'Foo2 Bar'
                 u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name': new_name})
 
-                self.assertEquals(u.id, u2.id)
-                self.assertEquals(u2.info['name'], new_name)
+                self.assertEqual(u.id, u2.id)
+                self.assertEqual(u2.info['name'], new_name)
 
 
     def test_can_create_election(self):
         """
         check that auth systems have the can_create_election call and that it's true for the common ones
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             assert(hasattr(auth_system_module, 'can_create_election'))
             if auth_system != 'clever':
                 assert(auth_system_module.can_create_election('foobar', {}))
@@ -62,13 +61,13 @@ class UserModelTests(unittest.TestCase):
         check that a user set up with status update ability reports it as such,
         and otherwise does not report it
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
 
             if hasattr(auth_system_module, 'send_message'):
-                self.assertNotEquals(u.update_status_template, None)
+                self.assertNotEqual(u.update_status_template, None)
             else:
-                self.assertEquals(u.update_status_template, None)
+                self.assertEqual(u.update_status_template, None)
 
     def test_eligibility(self):
         """
@@ -76,22 +75,18 @@ class UserModelTests(unittest.TestCase):
 
         FIXME: also test constraints on eligibility
         """
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'})
 
             self.assertTrue(u.is_eligible_for({'auth_system': auth_system}))
 
     def test_eq(self):
-        for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
+        for auth_system, auth_system_module in AUTH_SYSTEMS.items():
             u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
             u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'})
 
-            self.assertEquals(u, u2)
-
+            self.assertEqual(u, u2)
 
-import views
-import auth_systems.password as password_views
-from django.urls import reverse
 
 # FIXME: login CSRF should make these tests more complicated
 # and should be tested for
@@ -125,6 +120,6 @@ class UserBlackboxTests(TestCase):
         """using the test email backend"""
         self.test_user.send_message("testing subject", "testing body")
 
-        self.assertEquals(len(mail.outbox), 1)
-        self.assertEquals(mail.outbox[0].subject, "testing subject")
-        self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>")
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].subject, "testing subject")
+        self.assertEqual(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>")
diff --git a/helios_auth/urls.py b/helios_auth/urls.py
index 5244e10b8a1e2f07f8ab712c3b8f3997e58bedfa..43f552dd808135eadfe1faa041cde7e95686596e 100644
--- a/helios_auth/urls.py
+++ b/helios_auth/urls.py
@@ -7,9 +7,8 @@ Ben Adida (ben@adida.net)
 
 from django.conf.urls import url
 
-import url_names
-import views
 from settings import AUTH_ENABLED_AUTH_SYSTEMS
+from . import views, url_names
 
 urlpatterns = [
     # basic static stuff
@@ -24,10 +23,10 @@ urlpatterns = [
 
 # password auth
 if 'password' in AUTH_ENABLED_AUTH_SYSTEMS:
-    from auth_systems.password import urlpatterns as password_patterns
+    from .auth_systems.password import urlpatterns as password_patterns
     urlpatterns.extend(password_patterns)
 
 # twitter
 if 'twitter' in AUTH_ENABLED_AUTH_SYSTEMS:
-    from auth_systems.twitter import urlpatterns as twitter_patterns
+    from .auth_systems.twitter import urlpatterns as twitter_patterns
     urlpatterns.extend(twitter_patterns)
diff --git a/helios_auth/utils.py b/helios_auth/utils.py
index f57dedf6e961e8598963049dca55f6cdaf211f00..dc723adce77a3f08c19e1a0438a8ae20f611e882 100644
--- a/helios_auth/utils.py
+++ b/helios_auth/utils.py
@@ -7,23 +7,30 @@ Some basic utils
 
 import json
 
+
 ## JSON
 def to_json(d):
-  return json.dumps(d, sort_keys=True)
-  
-def from_json(json_str):
-  if not json_str: return None
-  return json.loads(json_str)
-  
-def JSONtoDict(json):
-    x=json.loads(json)
-    return x
-    
-def JSONFiletoDict(filename):
-  f = open(filename, 'r')
-  content = f.read()
-  f.close()
-  return JSONtoDict(content)
-    
+    return json.dumps(d, sort_keys=True)
+
+
+def from_json(value):
+    if value == "" or value is None:
+        return None
 
+    if isinstance(value, str):
+        try:
+            return json.loads(value)
+        except Exception as e:
+            # import ast
+            # try:
+            #     parsed_value = ast.literal_eval(parsed_value)
+            # except Exception as e1:
+            raise Exception("value is not JSON parseable, that's bad news") from e
 
+    return value
+
+
+def JSONFiletoDict(filename):
+    with open(filename, 'r') as f:
+        content = f.read()
+    return from_json(content)
diff --git a/helios_auth/view_utils.py b/helios_auth/view_utils.py
index 6699ab28ab5030b4b8703ea4868f5ef1edd22a14..48b90c7806167ee9031a3c5e55a82f03adf9af37 100644
--- a/helios_auth/view_utils.py
+++ b/helios_auth/view_utils.py
@@ -52,7 +52,3 @@ def render_template_raw(request, template_name, values=None):
   vars_with_user = prepare_vars(request, values)
 
   return t.render(context=vars_with_user, request=request)
-
-
-def render_json(json_txt):
-  return HttpResponse(json_txt)
diff --git a/helios_auth/views.py b/helios_auth/views.py
index f246dafd582ef696d6ba2351fad93d507f7924eb..73f23050ba2f9767a94da17a2e5b180578c4c6be 100644
--- a/helios_auth/views.py
+++ b/helios_auth/views.py
@@ -5,19 +5,19 @@ Ben Adida
 2009-07-05
 """
 
-import urllib
-from django.urls import reverse
+from urllib.parse import urlencode
+
 from django.http import HttpResponseRedirect, HttpResponse
+from django.urls import reverse
 
-import helios_auth
 import settings
-from auth_systems import AUTH_SYSTEMS
-from auth_systems import password
+from helios_auth import DEFAULT_AUTH_SYSTEM, ENABLED_AUTH_SYSTEMS
 from helios_auth.security import get_user
 from helios_auth.url_names import AUTH_INDEX, AUTH_START, AUTH_AFTER, AUTH_WHY, AUTH_AFTER_INTERVENTION
-from models import User
-from security import FIELDS_TO_SAVE
-from view_utils import render_template, render_template_raw
+from .auth_systems import AUTH_SYSTEMS, password
+from .models import User
+from .security import FIELDS_TO_SAVE
+from .view_utils import render_template, render_template_raw
 
 
 def index(request):
@@ -28,21 +28,21 @@ def index(request):
   user = get_user(request)
 
   # single auth system?
-  if len(helios_auth.ENABLED_AUTH_SYSTEMS) == 1 and not user:
-    return HttpResponseRedirect(reverse(AUTH_START, args=[helios_auth.ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
+  if len(ENABLED_AUTH_SYSTEMS) == 1 and not user:
+    return HttpResponseRedirect(reverse(AUTH_START, args=[ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', ''))
 
-  #if helios_auth.DEFAULT_AUTH_SYSTEM and not user:
-  #  return HttpResponseRedirect(reverse(start, args=[helios_auth.DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
+  #if DEFAULT_AUTH_SYSTEM and not user:
+  #  return HttpResponseRedirect(reverse(start, args=[DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', ''))
   
   default_auth_system_obj = None
-  if helios_auth.DEFAULT_AUTH_SYSTEM:
-    default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+  if DEFAULT_AUTH_SYSTEM:
+    default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
 
   #form = password.LoginForm()
 
   return render_template(request, 'index', {'return_url' : request.GET.get('return_url', '/'),
-                                            'enabled_auth_systems' : helios_auth.ENABLED_AUTH_SYSTEMS,
-                                            'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM,
+                                            'enabled_auth_systems' : ENABLED_AUTH_SYSTEMS,
+                                            'default_auth_system': DEFAULT_AUTH_SYSTEM,
                                             'default_auth_system_obj': default_auth_system_obj})
 
 def login_box_raw(request, return_url='/', auth_systems = None):
@@ -50,20 +50,20 @@ def login_box_raw(request, return_url='/', auth_systems = None):
   a chunk of HTML that shows the various login options
   """
   default_auth_system_obj = None
-  if helios_auth.DEFAULT_AUTH_SYSTEM:
-    default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM]
+  if DEFAULT_AUTH_SYSTEM:
+    default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM]
 
   # make sure that auth_systems includes only available and enabled auth systems
   if auth_systems is not None:
-    enabled_auth_systems = set(auth_systems).intersection(set(helios_auth.ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
+    enabled_auth_systems = set(auth_systems).intersection(set(ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys()))
   else:
-    enabled_auth_systems = set(helios_auth.ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
+    enabled_auth_systems = set(ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys()))
 
   form = password.LoginForm()
 
   return render_template_raw(request, 'login_box', {
       'enabled_auth_systems': enabled_auth_systems, 'return_url': return_url,
-      'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
+      'default_auth_system': DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj,
       'form' : form})
   
 def do_local_logout(request):
@@ -74,7 +74,7 @@ def do_local_logout(request):
 
   user = None
 
-  if request.session.has_key('user'):
+  if 'user' in request.session:
     user = request.session['user']
     
   # 2010-08-14 be much more aggressive here
@@ -151,7 +151,7 @@ def _do_auth(request):
     return HttpResponse("an error occurred trying to contact " + system_name +", try again later")
   
 def start(request, system_name):
-  if not (system_name in helios_auth.ENABLED_AUTH_SYSTEMS):
+  if not (system_name in ENABLED_AUTH_SYSTEMS):
     return HttpResponseRedirect(reverse(AUTH_INDEX))
   
   # why is this here? Let's try without it
@@ -173,7 +173,7 @@ def perms_why(request):
 
 def after(request):
   # which auth system were we using?
-  if not request.session.has_key('auth_system_name'):
+  if 'auth_system_name' not in request.session:
     do_local_logout(request)
     return HttpResponseRedirect("/")
     
@@ -188,7 +188,7 @@ def after(request):
     
     request.session['user'] = user
   else:
-    return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urllib.urlencode({'system_name' : request.session['auth_system_name']})))
+    return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urlencode({'system_name' : request.session['auth_system_name']})))
 
   # does the auth system want to present an additional view?
   # this is, for example, to prompt the user to follow @heliosvoting
@@ -203,7 +203,7 @@ def after(request):
 
 def after_intervention(request):
   return_url = "/"
-  if request.session.has_key('auth_return_url'):
+  if 'auth_return_url' in request.session:
     return_url = request.session['auth_return_url']
     del request.session['auth_return_url']
   return HttpResponseRedirect(settings.URL_HOST + return_url)
diff --git a/requirements.txt b/requirements.txt
index d779153ddf26e55b5d21249a094de8481a5063f0..414d5f26982d0769a3988be987cb698c15412f43 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,21 +4,20 @@ celery==4.2.1
 django-picklefield==0.3.0
 kombu==4.2.0
 html5lib==0.999
-psycopg2==2.7.3.2
-pyparsing==1.5.7
+psycopg2==2.8.3
+pyparsing==2.0.0
 python-dateutil>=1.5
-python-openid==2.2.5
-wsgiref==0.1.2
+python3-openid==3.0.10
 gunicorn==19.9
 requests==2.21.0
-unicodecsv==0.9.0
+unicodecsv==0.14.1
 dj_database_url==0.3.0
 django_webtest>=1.9
 webtest==2.0.18
 bleach==1.4.1
-boto==2.27.0
-django-ses==0.6.0
-validate_email==1.2
-oauth2client==1.2
+boto==2.49.0
+django-ses==0.8.10
+py3-validate-email==0.1.11
+oauth2client==4.1.3
 rollbar==0.12.1
 pycryptodome==3.8.2
diff --git a/server_ui/urls.py b/server_ui/urls.py
index e691c07afe7b29c0b347e5bde5ea75e9addd65c7..aad77654242dcfa268cc23b7103de800b68b9dfa 100644
--- a/server_ui/urls.py
+++ b/server_ui/urls.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from django.conf.urls import url
 
-from views import home, about, docs, faq, privacy
+from .views import home, about, docs, faq, privacy
 
 urlpatterns = [
   url(r'^$', home),
diff --git a/server_ui/view_utils.py b/server_ui/view_utils.py
index df947214620fc0169b9569767a84e633cd0e7615..f499603e108fdbb3db9a352df2e0fb6efb4ab2e9 100644
--- a/server_ui/view_utils.py
+++ b/server_ui/view_utils.py
@@ -20,7 +20,7 @@ def render_template(request, template_name, values = None):
   vars_with_user['CURRENT_URL'] = request.path
   
   # csrf protection
-  if request.session.has_key('csrf_token'):
+  if 'csrf_token' in request.session:
     vars_with_user['csrf_token'] = request.session['csrf_token']
   
   return render_to_response('server_ui/templates/%s.html' % template_name, vars_with_user)
diff --git a/server_ui/views.py b/server_ui/views.py
index 190d902aa71f4c0722ed68a787055a4f978ba7c8..2dbaa1ad2b01c2e72dbb4976cb83853a5cb7d554 100644
--- a/server_ui/views.py
+++ b/server_ui/views.py
@@ -3,15 +3,15 @@ server_ui specific views
 """
 
 import copy
+
 from django.conf import settings
 
 import helios_auth.views as auth_views
 from helios.models import Election
 from helios.security import can_create_election
 from helios_auth.security import get_user
-from view_utils import render_template
-import glue
-
+from . import glue
+from .view_utils import render_template
 
 glue.glue()  # actually apply glue helios.view <-> helios.signals
 
diff --git a/settings.py b/settings.py
index 32769792e235207c8e79a46942056972dcacf343..13b9069f694427b77490bda31e6d1b4e820f596c 100644
--- a/settings.py
+++ b/settings.py
@@ -9,7 +9,7 @@ TESTING = 'test' in sys.argv
 
 # go through environment variables and override them
 def get_from_env(var, default):
-    if not TESTING and os.environ.has_key(var):
+    if not TESTING and var in os.environ:
         return os.environ[var]
     else:
         return default
@@ -39,7 +39,7 @@ DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.postgresql_psycopg2',
         'NAME': 'helios'
-    }
+    },
 }
 
 # override if we have an env variable
@@ -277,7 +277,7 @@ if TESTING:
 # Rollbar Error Logging
 ROLLBAR_ACCESS_TOKEN = get_from_env('ROLLBAR_ACCESS_TOKEN', None)
 if ROLLBAR_ACCESS_TOKEN:
-  print "setting up rollbar"
+  print("setting up rollbar")
   MIDDLEWARE += ['rollbar.contrib.django.middleware.RollbarNotifierMiddleware',]
   ROLLBAR = {
     'access_token': ROLLBAR_ACCESS_TOKEN,