Author: Dirk Thomas
Support both python2 and python 3.
--- a/em.py
+++ b/em.py
@@ -17,22 +17,16 @@
 
 import copy
 import getopt
+import inspect
 import os
 import re
-import string
 import sys
 import types
 
 try:
-    # The equivalent of import cStringIO as StringIO.
-    import cStringIO
-    StringIO = cStringIO
-    del cStringIO
+    from cStringIO import StringIO
 except ImportError:
-    import StringIO
-
-# For backward compatibility, we can't assume these are defined.
-False, True = 0, 1
+    from io import StringIO
 
 # Some basic defaults.
 FAILURE_CODE = 1
@@ -298,9 +292,9 @@
         self.exc = exc
 
     def __str__(self):
-        backtrace = map(lambda x: str(x), self.contexts)
+        backtrace = [str(x) for x in self.contexts]
         return "%s: %s (%s)" % (self.exc.__class__, self.exc, \
-                                (string.join(backtrace, ', ')))
+                                (', '.join(backtrace)))
 
 
 class Subsystem:
@@ -318,10 +312,10 @@
                    inputErrors=None, outputErrors=None):
         self.useUnicode = True
         try:
-            unicode
+            str
             import codecs
         except (NameError, ImportError):
-            raise SubsystemError, "Unicode subsystem unavailable"
+            raise SubsystemError("Unicode subsystem unavailable")
         defaultEncoding = sys.getdefaultencoding()
         if inputEncoding is None:
             inputEncoding = defaultEncoding
@@ -338,7 +332,7 @@
 
     def assertUnicode(self):
         if not self.useUnicode:
-            raise SubsystemError, "Unicode subsystem unavailable"
+            raise SubsystemError("Unicode subsystem unavailable")
 
     def open(self, name, mode=None):
         if self.useUnicode:
@@ -381,14 +375,14 @@
         try:
             return self.data[-1]
         except IndexError:
-            raise StackUnderflowError, "stack is empty for top"
+            raise StackUnderflowError("stack is empty for top")
         
     def pop(self):
         """Pop the top element off the stack and return it."""
         try:
             return self.data.pop()
         except IndexError:
-            raise StackUnderflowError, "stack is empty for pop"
+            raise StackUnderflowError("stack is empty for pop")
         
     def push(self, object):
         """Push an element onto the top of the stack."""
@@ -396,7 +390,7 @@
 
     def filter(self, function):
         """Filter the elements of the stack through the function."""
-        self.data = filter(function, self.data)
+        self.data = list(filter(function, self.data))
 
     def purge(self):
         """Purge the stack."""
@@ -406,14 +400,14 @@
         """Create a duplicate of this stack."""
         return self.__class__(self.data[:])
 
-    def __nonzero__(self): return len(self.data) != 0
+    def __bool__(self): return len(self.data) != 0
     def __len__(self): return len(self.data)
     def __getitem__(self, index): return self.data[-(index + 1)]
 
     def __repr__(self):
         return '<%s instance at 0x%x [%s]>' % \
                (self.__class__, id(self), \
-                string.join(map(repr, self.data), ', '))
+                ', '.join(list(map(repr, self.data))))
 
 
 class AbstractFile:
@@ -430,7 +424,7 @@
         self.mode = mode
         self.buffered = buffered
         if buffered:
-            self.bufferFile = StringIO.StringIO()
+            self.bufferFile = StringIO()
         else:
             self.bufferFile = theSubsystem.open(filename, mode)
         # Okay, we got this far, so the AbstractFile is initialized.
@@ -478,7 +472,7 @@
     strings or (readable) file objects."""
 
     def __init__(self):
-        self.file = StringIO.StringIO()
+        self.file = StringIO()
 
     # These methods define the writable file-like interface for the
     # diversion.
@@ -504,7 +498,7 @@
 
     def asFile(self):
         """Return the diversion as a file."""
-        return StringIO.StringIO(self.file.getvalue())
+        return StringIO(self.file.getvalue())
 
 
 class Stream:
@@ -544,15 +538,16 @@
         independently."""
         if shortcut == 0:
             return NullFilter()
-        elif type(shortcut) is types.FunctionType or \
-             type(shortcut) is types.BuiltinFunctionType or \
-             type(shortcut) is types.BuiltinMethodType or \
-             type(shortcut) is types.LambdaType:
+        elif isinstance(shortcut, types.FunctionType) or \
+             inspect.ismethoddescriptor(shortcut) or \
+             isinstance(shortcut, types.BuiltinFunctionType) or \
+             isinstance(shortcut, types.BuiltinMethodType) or \
+             isinstance(shortcut, types.LambdaType):
             return FunctionFilter(shortcut)
-        elif type(shortcut) is types.StringType:
+        elif isinstance(shortcut, bytes):
             return StringFilter(filter)
-        elif type(shortcut) is types.DictType:
-            raise NotImplementedError, "mapping filters not yet supported"
+        elif isinstance(shortcut, dict):
+            raise NotImplementedError("mapping filters not yet supported")
         else:
             # Presume it's a plain old filter.
             return shortcut
@@ -565,7 +560,7 @@
         thisFilter, lastFilter = self.filter, None
         while thisFilter is not None and thisFilter is not self.file:
             lastFilter = thisFilter
-            thisFilter = thisFilter.next()
+            thisFilter = thisFilter.__next__()
         return lastFilter
 
     def install(self, shortcut=None):
@@ -577,7 +572,7 @@
             # Shortcuts for "no filter."
             self.filter = self.file
         else:
-            if type(shortcut) in (types.ListType, types.TupleType):
+            if type(shortcut) in (list, tuple):
                 shortcuts = list(shortcut)
             else:
                 shortcuts = [shortcut]
@@ -626,43 +621,43 @@
         """Create a diversion if one does not already exist, but do not
         divert to it yet."""
         if name is None:
-            raise DiversionError, "diversion name must be non-None"
-        if not self.diversions.has_key(name):
+            raise DiversionError("diversion name must be non-None")
+        if name not in self.diversions:
             self.diversions[name] = Diversion()
 
     def retrieve(self, name):
         """Retrieve the given diversion."""
         if name is None:
-            raise DiversionError, "diversion name must be non-None"
-        if self.diversions.has_key(name):
+            raise DiversionError("diversion name must be non-None")
+        if name in self.diversions:
             return self.diversions[name]
         else:
-            raise DiversionError, "nonexistent diversion: %s" % name
+            raise DiversionError("nonexistent diversion: %s" % name)
 
     def divert(self, name):
         """Start diverting."""
         if name is None:
-            raise DiversionError, "diversion name must be non-None"
+            raise DiversionError("diversion name must be non-None")
         self.create(name)
         self.currentDiversion = name
 
     def undivert(self, name, purgeAfterwards=False):
         """Undivert a particular diversion."""
         if name is None:
-            raise DiversionError, "diversion name must be non-None"
-        if self.diversions.has_key(name):
+            raise DiversionError("diversion name must be non-None")
+        if name in self.diversions:
             diversion = self.diversions[name]
             self.filter.write(diversion.asString())
             if purgeAfterwards:
                 self.purge(name)
         else:
-            raise DiversionError, "nonexistent diversion: %s" % name
+            raise DiversionError("nonexistent diversion: %s" % name)
 
     def purge(self, name):
         """Purge the specified diversion."""
         if name is None:
-            raise DiversionError, "diversion name must be non-None"
-        if self.diversions.has_key(name):
+            raise DiversionError("diversion name must be non-None")
+        if name in self.diversions:
             del self.diversions[name]
             if self.currentDiversion == name:
                 self.currentDiversion = None
@@ -671,8 +666,7 @@
         """Undivert all pending diversions."""
         if self.diversions:
             self.revert() # revert before undiverting!
-            names = self.diversions.keys()
-            names.sort()
+            names = sorted(list(self.diversions.keys()))
             for name in names:
                 self.undivert(name)
                 if purgeAfterwards:
@@ -775,7 +769,7 @@
             raise NotImplementedError
         self.sink = None
 
-    def next(self):
+    def __next__(self):
         """Return the next filter/file-like object in the sequence, or None."""
         return self.sink
 
@@ -821,7 +815,7 @@
         this, last = self, self
         while this is not None:
             last = this
-            this = this.next()
+            this = this.__next__()
         return last
 
 class NullFilter(Filter):
@@ -848,13 +842,13 @@
     filters any incoming data through it."""
 
     def __init__(self, table):
-        if not (type(table) == types.StringType and len(table) == 256):
-            raise FilterError, "table must be 256-character string"
+        if not (isinstance(table, bytes) and len(table) == 256):
+            raise FilterError("table must be 256-character string")
         Filter.__init__(self)
         self.table = table
 
     def write(self, data):
-        self.sink.write(string.translate(data, self.table))
+        self.sink.write(data.translate(self.table))
 
 class BufferedFilter(Filter):
 
@@ -901,7 +895,7 @@
 
     def write(self, data):
         BufferedFilter.write(self, data)
-        chunks = string.split(self.buffer, '\n')
+        chunks = self.buffer.split('\n')
         for chunk in chunks[:-1]:
             self.sink.write(chunk + '\n')
         self.buffer = chunks[-1]
@@ -963,7 +957,7 @@
 
     def deregister(self, interpreter):
         if interpreter is not self.interpreter:
-            raise Error, "hook not associated with this interpreter"
+            raise Error("hook not associated with this interpreter")
         self.interpreter = None
 
     def push(self):
@@ -1133,7 +1127,7 @@
         if loc >= 0:
             self.comment = scanner.chop(loc, 1)
         else:
-            raise TransientParseError, "comment expects newline"
+            raise TransientParseError("comment expects newline")
 
     def string(self):
         return '%s#%s\n' % (self.prefix, self.comment)
@@ -1143,9 +1137,9 @@
     def scan(self, scanner):
         loc = scanner.find('\n')
         if loc >= 0:
-            self.name = string.strip(scanner.chop(loc, 1))
+            self.name = scanner.chop(loc, 1).strip()
         else:
-            raise TransientParseError, "context name expects newline"
+            raise TransientParseError("context name expects newline")
 
     def run(self, interpreter, locals):
         context = interpreter.context()
@@ -1159,9 +1153,9 @@
             try:
                 self.line = int(scanner.chop(loc, 1))
             except ValueError:
-                raise ParseError, "context line requires integer"
+                raise ParseError("context line requires integer")
         else:
-            raise TransientParseError, "context line expects newline"
+            raise TransientParseError("context line expects newline")
 
     def run(self, interpreter, locals):
         context = interpreter.context()
@@ -1184,7 +1178,7 @@
                 result = '\x08'
             elif code == 'd': # decimal code
                 decimalCode = scanner.chop(3)
-                result = chr(string.atoi(decimalCode, 10))
+                result = chr(int(decimalCode, 10))
             elif code == 'e': # ESC
                 result = '\x1b'
             elif code == 'f': # FF
@@ -1197,20 +1191,19 @@
                 theSubsystem.assertUnicode()
                 import unicodedata
                 if scanner.chop(1) != '{':
-                    raise ParseError, r"Unicode name escape should be \N{...}"
+                    raise ParseError(r"Unicode name escape should be \N{...}")
                 i = scanner.find('}')
                 name = scanner.chop(i, 1)
                 try:
                     result = unicodedata.lookup(name)
                 except KeyError:
-                    raise SubsystemError, \
-                          "unknown Unicode character name: %s" % name
+                    raise SubsystemError("unknown Unicode character name: %s" % name)
             elif code == 'o': # octal code
                 octalCode = scanner.chop(3)
-                result = chr(string.atoi(octalCode, 8))
+                result = chr(int(octalCode, 8))
             elif code == 'q': # quaternary code
                 quaternaryCode = scanner.chop(4)
-                result = chr(string.atoi(quaternaryCode, 4))
+                result = chr(int(quaternaryCode, 4))
             elif code == 'r': # CR
                 result = '\x0d'
             elif code in 's ': # SP
@@ -1220,32 +1213,32 @@
             elif code in 'u': # Unicode 16-bit hex literal
                 theSubsystem.assertUnicode()
                 hexCode = scanner.chop(4)
-                result = unichr(string.atoi(hexCode, 16))
+                result = chr(int(hexCode, 16))
             elif code in 'U': # Unicode 32-bit hex literal
                 theSubsystem.assertUnicode()
                 hexCode = scanner.chop(8)
-                result = unichr(string.atoi(hexCode, 16))
+                result = chr(int(hexCode, 16))
             elif code == 'v': # VT
                 result = '\x0b'
             elif code == 'x': # hexadecimal code
                 hexCode = scanner.chop(2)
-                result = chr(string.atoi(hexCode, 16))
+                result = chr(int(hexCode, 16))
             elif code == 'z': # EOT
                 result = '\x04'
             elif code == '^': # control character
-                controlCode = string.upper(scanner.chop(1))
+                controlCode = scanner.chop(1).upper()
                 if controlCode >= '@' and controlCode <= '`':
                     result = chr(ord(controlCode) - ord('@'))
                 elif controlCode == '?':
                     result = '\x7f'
                 else:
-                    raise ParseError, "invalid escape control code"
+                    raise ParseError("invalid escape control code")
             else:
-                raise ParseError, "unrecognized escape code"
+                raise ParseError("unrecognized escape code")
             assert result is not None
             self.code = result
         except ValueError:
-            raise ParseError, "invalid numeric escape code"
+            raise ParseError("invalid numeric escape code")
 
     def run(self, interpreter, locals):
         interpreter.write(self.code)
@@ -1260,13 +1253,13 @@
         if loc >= 0:
             line = scanner.chop(loc, 1)
             if not line:
-                raise ParseError, "significator must have nonblank key"
+                raise ParseError("significator must have nonblank key")
             if line[0] in ' \t\v\n':
-                raise ParseError, "no whitespace between % and key"
+                raise ParseError("no whitespace between % and key")
             # Work around a subtle CPython-Jython difference by stripping
             # the string before splitting it: 'a '.split(None, 1) has two
             # elements in Jython 2.1).
-            fields = string.split(string.strip(line), None, 1)
+            fields = line.strip().split(None, 1)
             if len(fields) == 2 and fields[1] == '':
                 fields.pop()
             self.key = fields[0]
@@ -1274,12 +1267,12 @@
                 fields.append(None)
             self.key, self.valueCode = fields
         else:
-            raise TransientParseError, "significator expects newline"
+            raise TransientParseError("significator expects newline")
 
     def run(self, interpreter, locals):
         value = self.valueCode
         if value is not None:
-            value = interpreter.evaluate(string.strip(value), locals)
+            value = interpreter.evaluate(value.strip(), locals)
         interpreter.significate(self.key, value)
 
     def string(self):
@@ -1440,7 +1433,7 @@
         scanner.acquire()
         i = scanner.complex('[', ']', 0)
         self.contents = scanner.chop(i, 1)
-        fields = string.split(string.strip(self.contents), ' ', 1)
+        fields = self.contents.strip().split(' ', 1)
         if len(fields) > 1:
             self.type, self.rest = fields
         else:
@@ -1448,7 +1441,7 @@
             self.rest = None
         self.subtokens = []
         if self.type in self.GREEDY_TYPES and self.rest is None:
-            raise ParseError, "control '%s' needs arguments" % self.type
+            raise ParseError("control '%s' needs arguments" % self.type)
         if self.type in self.PRIMARY_TYPES:
             self.subscan(scanner, self.type)
             self.kind = 'primary'
@@ -1459,7 +1452,7 @@
         elif self.type in self.END_TYPES:
             self.kind = 'end'
         else:
-            raise ParseError, "unknown control markup: '%s'" % self.type
+            raise ParseError("unknown control markup: '%s'" % self.type)
         scanner.release()
 
     def subscan(self, scanner, primary):
@@ -1467,13 +1460,11 @@
         while True:
             token = scanner.one()
             if token is None:
-                raise TransientParseError, \
-                      "control '%s' needs more tokens" % primary
+                raise TransientParseError("control '%s' needs more tokens" % primary)
             if isinstance(token, ControlToken) and \
                    token.type in self.END_TYPES:
                 if token.rest != primary:
-                    raise ParseError, \
-                          "control must end with 'end %s'" % primary
+                    raise ParseError("control must end with 'end %s'" % primary)
                 break
             self.subtokens.append(token)
 
@@ -1492,8 +1483,7 @@
             if isinstance(subtoken, ControlToken) and \
                subtoken.kind == 'secondary':
                 if subtoken.type not in allowed:
-                    raise ParseError, \
-                          "control unexpected secondary: '%s'" % subtoken.type
+                    raise ParseError("control unexpected secondary: '%s'" % subtoken.type)
                 latest = []
                 result.append((subtoken, latest))
             else:
@@ -1510,8 +1500,7 @@
                 elseTokens = info.pop()[1]
             for secondary, subtokens in info:
                 if secondary.type not in ('if', 'elif'):
-                    raise ParseError, \
-                          "control 'if' unexpected secondary: '%s'" % secondary.type
+                    raise ParseError("control 'if' unexpected secondary: '%s'" % secondary.type)
                 if interpreter.evaluate(secondary.rest, locals):
                     self.subrun(subtokens, interpreter, locals)
                     break
@@ -1521,14 +1510,14 @@
         elif self.type == 'for':
             sides = self.IN_RE.split(self.rest, 1)
             if len(sides) != 2:
-                raise ParseError, "control expected 'for x in seq'"
+                raise ParseError("control expected 'for x in seq'")
             iterator, sequenceCode = sides
             info = self.build(['else'])
             elseTokens = None
             if info[-1][0].type == 'else':
                 elseTokens = info.pop()[1]
             if len(info) != 1:
-                raise ParseError, "control 'for' expects at most one 'else'"
+                raise ParseError("control 'for' expects at most one 'else'")
             sequence = interpreter.evaluate(sequenceCode, locals)
             for element in sequence:
                 try:
@@ -1548,7 +1537,7 @@
             if info[-1][0].type == 'else':
                 elseTokens = info.pop()[1]
             if len(info) != 1:
-                raise ParseError, "control 'while' expects at most one 'else'"
+                raise ParseError("control 'while' expects at most one 'else'")
             atLeastOnce = False
             while True:
                 try:
@@ -1565,24 +1554,22 @@
         elif self.type == 'try':
             info = self.build(['except', 'finally'])
             if len(info) == 1:
-                raise ParseError, "control 'try' needs 'except' or 'finally'"
+                raise ParseError("control 'try' needs 'except' or 'finally'")
             type = info[-1][0].type
             if type == 'except':
                 for secondary, _tokens in info[1:]:
                     if secondary.type != 'except':
-                        raise ParseError, \
-                              "control 'try' cannot have 'except' and 'finally'"
+                        raise ParseError("control 'try' cannot have 'except' and 'finally'")
             else:
                 assert type == 'finally'
                 if len(info) != 2:
-                    raise ParseError, \
-                          "control 'try' can only have one 'finally'"
+                    raise ParseError("control 'try' can only have one 'finally'")
             if type == 'except':
                 try:
                     self.subrun(info[0][1], interpreter, locals)
                 except FlowError:
                     raise
-                except Exception, e:
+                except Exception as e:
                     for secondary, tokens in info[1:]:
                         exception, variable = interpreter.clause(secondary.rest)
                         if variable is not None:
@@ -1598,9 +1585,9 @@
                 finally:
                     self.subrun(info[1][1], interpreter, locals)
         elif self.type == 'continue':
-            raise ContinueFlow, "control 'continue' without 'for', 'while'"
+            raise ContinueFlow("control 'continue' without 'for', 'while'")
         elif self.type == 'break':
-            raise BreakFlow, "control 'break' without 'for', 'while'"
+            raise BreakFlow("control 'break' without 'for', 'while'")
         elif self.type == 'def':
             signature = self.rest
             definition = self.substring()
@@ -1610,10 +1597,9 @@
                    (signature, definition, interpreter.pseudo, definition)
             interpreter.execute(code, locals)
         elif self.type == 'end':
-            raise ParseError, "control 'end' requires primary markup"
+            raise ParseError("control 'end' requires primary markup")
         else:
-            raise ParseError, \
-                  "control '%s' cannot be at this level" % self.type
+            raise ParseError("control '%s' cannot be at this level" % self.type)
         interpreter.invoke('afterControl')
 
     def subrun(self, tokens, interpreter, locals):
@@ -1622,7 +1608,7 @@
             token.run(interpreter, locals)
 
     def substring(self):
-        return string.join(map(str, self.subtokens), '')
+        return ''.join(list(map(str, self.subtokens)))
 
     def string(self):
         if self.kind == 'primary':
@@ -1666,9 +1652,12 @@
         self.buffer = data
         self.lock = 0
 
-    def __nonzero__(self): return self.pointer < len(self.buffer)
+    def __bool__(self): return self.pointer < len(self.buffer)
     def __len__(self): return len(self.buffer) - self.pointer
-    def __getitem__(self, index): return self.buffer[self.pointer + index]
+    def __getitem__(self, index):
+        if isinstance(index, slice):
+            return ''.join([self.buffer[x] for x in range(self.pointer + (index.start if index.start else 0), (self.pointer + index.stop) if index.stop is not None else len(self.buffer), index.step if index.step else 1)])
+        return self.buffer[self.pointer + index]
 
     def __getslice__(self, start, stop):
         if stop > len(self):
@@ -1682,7 +1671,7 @@
     def retreat(self, count=1):
         self.pointer = self.pointer - count
         if self.pointer < 0:
-            raise ParseError, "can't retreat back over synced out chars"
+            raise ParseError("can't retreat back over synced out chars")
 
     def set(self, data):
         """Start the scanner digesting a new batch of data; start the pointer
@@ -1702,7 +1691,7 @@
             assert slop == 0
             count = len(self)
         if count > len(self):
-            raise TransientParseError, "not enough data to read"
+            raise TransientParseError("not enough data to read")
         result = self[:count]
         self.advance(count + slop)
         return result
@@ -1735,7 +1724,7 @@
         """Read count chars starting from i; raise a transient error if
         there aren't enough characters remaining."""
         if len(self) < i + count:
-            raise TransientParseError, "need more data to read"
+            raise TransientParseError("need more data to read")
         else:
             return self[i:i + count]
 
@@ -1750,7 +1739,7 @@
                     if self[i] == quote:
                         return quote
                 else:
-                    raise TransientParseError, "need to scan for rest of quote"
+                    raise TransientParseError("need to scan for rest of quote")
             if self[i + 1] == self[i + 2] == quote:
                 quote = quote * 3
         if quote is not None:
@@ -1769,9 +1758,9 @@
     def find(self, sub, start=0, end=None):
         """Find the next occurrence of the character, or return -1."""
         if end is not None:
-            return string.find(self.rest(), sub, start, end)
+            return self.rest().find(sub, start, end)
         else:
-            return string.find(self.rest(), sub, start)
+            return self.rest().find(sub, start)
 
     def last(self, char, start=0, end=None):
         """Find the first character that is _not_ the specified character."""
@@ -1783,7 +1772,7 @@
                 return i
             i = i + 1
         else:
-            raise TransientParseError, "expecting other than %s" % char
+            raise TransientParseError("expecting other than %s" % char)
 
     def next(self, target, start=0, end=None, mandatory=False):
         """Scan for the next occurrence of one of the characters in
@@ -1813,9 +1802,9 @@
                 i = i + 1
         else:
             if mandatory:
-                raise ParseError, "expecting %s, not found" % target
+                raise ParseError("expecting %s, not found" % target)
             else:
-                raise TransientParseError, "expecting ending character"
+                raise TransientParseError("expecting ending character")
 
     def quote(self, start=0, end=None, mandatory=False):
         """Scan for the end of the next quote."""
@@ -1837,9 +1826,9 @@
                 i = i + 1
         else:
             if mandatory:
-                raise ParseError, "expecting end of string literal"
+                raise ParseError("expecting end of string literal")
             else:
-                raise TransientParseError, "expecting end of string literal"
+                raise TransientParseError("expecting end of string literal")
 
     def nested(self, enter, exit, start=0, end=None):
         """Scan from i for an ending sequence, respecting entries and exits
@@ -1858,7 +1847,7 @@
                     return i
             i = i + 1
         else:
-            raise TransientParseError, "expecting end of complex expression"
+            raise TransientParseError("expecting end of complex expression")
 
     def complex(self, enter, exit, start=0, end=None, skip=None):
         """Scan from i for an ending sequence, respecting quotes,
@@ -1893,7 +1882,7 @@
                 last = c
                 i = i + 1
         else:
-            raise TransientParseError, "expecting end of complex expression"
+            raise TransientParseError("expecting end of complex expression")
 
     def word(self, start=0):
         """Scan from i for a simple word."""
@@ -1904,7 +1893,7 @@
                 return i
             i = i + 1
         else:
-            raise TransientParseError, "expecting end of word"
+            raise TransientParseError("expecting end of word")
 
     def phrase(self, start=0):
         """Scan from i for a phrase (e.g., 'word', 'f(a, b, c)', 'a[i]', or
@@ -1914,7 +1903,7 @@
         while i < len(self) and self[i] in '([{':
             enter = self[i]
             if enter == '{':
-                raise ParseError, "curly braces can't open simple expressions"
+                raise ParseError("curly braces can't open simple expressions")
             exit = ENDING_CHARS[enter]
             i = self.complex(enter, exit, i + 1) + 1
         return i
@@ -1958,7 +1947,7 @@
                 elif first in firsts:
                     break
             else:
-                raise ParseError, "unknown markup: %s%s" % (self.prefix, first)
+                raise ParseError("unknown markup: %s%s" % (self.prefix, first))
             token = factory(self.prefix, first)
             try:
                 token.scan(self)
@@ -2087,16 +2076,16 @@
             self.globals = {}
         # Make sure that there is no collision between two interpreters'
         # globals.
-        if self.globals.has_key(self.pseudo):
+        if self.pseudo in self.globals:
             if self.globals[self.pseudo] is not self:
-                raise Error, "interpreter globals collision"
+                raise Error("interpreter globals collision")
         self.globals[self.pseudo] = self
 
     def unfix(self):
         """Remove the pseudomodule (if present) from the globals."""
         UNWANTED_KEYS = [self.pseudo, '__builtins__']
         for unwantedKey in UNWANTED_KEYS:
-            if self.globals.has_key(unwantedKey):
+            if unwantedKey in self.globals:
                 del self.globals[unwantedKey]
 
     def update(self, other):
@@ -2187,7 +2176,7 @@
 
     def include(self, fileOrFilename, locals=None):
         """Do an include pass on a file or filename."""
-        if type(fileOrFilename) is types.StringType:
+        if isinstance(fileOrFilename, bytes):
             # Either it's a string representing a filename ...
             filename = fileOrFilename
             name = filename
@@ -2202,7 +2191,7 @@
 
     def expand(self, data, locals=None):
         """Do an explicit expansion on a subordinate stream."""
-        outFile = StringIO.StringIO()
+        outFile = StringIO()
         stream = Stream(outFile)
         self.invoke('beforeExpand', string=data, locals=locals)
         self.streams.push(stream)
@@ -2230,7 +2219,7 @@
         except TransientParseError:
             pass
         result.append(data[i:])
-        result = string.join(result, '')
+        result = ''.join(result)
         self.invoke('afterQuote', result=result)
         return result
 
@@ -2242,7 +2231,7 @@
         for char in data:
             if char < ' ' or char > '~':
                 charOrd = ord(char)
-                if Interpreter.ESCAPE_CODES.has_key(charOrd):
+                if charOrd in Interpreter.ESCAPE_CODES:
                     result.append(self.prefix + '\\' + \
                                   Interpreter.ESCAPE_CODES[charOrd])
                 else:
@@ -2251,7 +2240,7 @@
                 result.append(self.prefix + '\\' + char)
             else:
                 result.append(char)
-        result = string.join(result, '')
+        result = ''.join(result)
         self.invoke('afterEscape', result=result)
         return result
 
@@ -2261,21 +2250,21 @@
         """Wrap around an application of a callable and handle errors.
         Return whether no error occurred."""
         try:
-            apply(callable, args)
+            callable(*args)
             self.reset()
             return True
-        except KeyboardInterrupt, e:
+        except KeyboardInterrupt as e:
             # Handle keyboard interrupts specially: we should always exit
             # from these.
             self.fail(e, True)
-        except Exception, e:
+        except Exception as e:
             # A standard exception (other than a keyboard interrupt).
             self.fail(e)
         except:
             # If we get here, then either it's an exception not derived from
             # Exception or it's a string exception, so get the error type
             # from the sys module.
-            e = sys.exc_type
+            e = sys.exc_info()[0]
             self.fail(e)
         # An error occurred if we leak through to here, so do cleanup.
         self.reset()
@@ -2327,7 +2316,7 @@
                 if self.options.get(BANGPATH_OPT, True) and self.prefix:
                     # Replace a bangpath at the beginning of the first line
                     # with an EmPy comment.
-                    if string.find(line, BANGPATH) == 0:
+                    if line.startswith(BANGPATH):
                         line = self.prefix + '#' + line[2:]
                 first = False
             if line:
@@ -2406,9 +2395,9 @@
         result = []
         stack = [result]
         for garbage in self.ASSIGN_TOKEN_RE.split(name):
-            garbage = string.strip(garbage)
+            garbage = garbage.strip()
             if garbage:
-                raise ParseError, "unexpected assignment token: '%s'" % garbage
+                raise ParseError("unexpected assignment token: '%s'" % garbage)
         tokens = self.ASSIGN_TOKEN_RE.findall(name)
         # While processing, put a None token at the start of any list in which
         # commas actually appear.
@@ -2459,12 +2448,12 @@
         try:
             values = tuple(values)
         except TypeError:
-            raise TypeError, "unpack non-sequence"
+            raise TypeError("unpack non-sequence")
         if len(names) != len(values):
-            raise ValueError, "unpack tuple of wrong size"
+            raise ValueError("unpack tuple of wrong size")
         for i in range(len(names)):
             name = names[i]
-            if type(name) is types.StringType:
+            if isinstance(name, bytes) or isinstance(name, str):
                 self.atomic(name, values[i], locals)
             else:
                 self.multi(name, values[i], locals)
@@ -2475,7 +2464,7 @@
         left = self.tokenize(name)
         # The return value of tokenize can either be a string or a list of
         # (lists of) strings.
-        if type(left) is types.StringType:
+        if isinstance(left, bytes) or  isinstance(left, str):
             self.atomic(left, value, locals)
         else:
             self.multi(left, value, locals)
@@ -2493,11 +2482,11 @@
         self.invoke('beforeClause', catch=catch, locals=locals)
         if catch is None:
             exceptionCode, variable = None, None
-        elif string.find(catch, ',') >= 0:
-            exceptionCode, variable = string.split(string.strip(catch), ',', 1)
-            variable = string.strip(variable)
+        elif catch.find(',') >= 0:
+            exceptionCode, variable = catch.strip().split(',', 1)
+            variable = variable.strip()
         else:
-            exceptionCode, variable = string.strip(catch), None
+            exceptionCode, variable = catch.strip(), None
         if not exceptionCode:
             exception = Exception
         else:
@@ -2520,11 +2509,11 @@
         defined either in the locals or the globals."""
         self.invoke('beforeDefined', name=name, local=local)
         if locals is not None:
-            if locals.has_key(name):
+            if name in locals:
                 result = True
             else:
                 result = False
-        elif self.globals.has_key(name):
+        elif name in self.globals:
             result = True
         else:
             result = False
@@ -2560,20 +2549,20 @@
         # If there are any carriage returns (as opposed to linefeeds/newlines)
         # in the statements code, then remove them.  Even on DOS/Windows
         # platforms, 
-        if string.find(statements, '\r') >= 0:
-            statements = string.replace(statements, '\r', '')
+        if statements.find('\r') >= 0:
+            statements = statements.replace('\r', '')
         # If there are no newlines in the statements code, then strip any
         # leading or trailing whitespace.
-        if string.find(statements, '\n') < 0:
-            statements = string.strip(statements)
+        if statements.find('\n') < 0:
+            statements = statements.strip()
         self.push()
         try:
             self.invoke('beforeExecute', \
                         statements=statements, locals=locals)
             if locals is not None:
-                exec statements in self.globals, locals
+                exec(statements, self.globals, locals)
             else:
-                exec statements in self.globals
+                exec(statements, self.globals)
             self.invoke('afterExecute')
         finally:
             self.pop()
@@ -2587,9 +2576,9 @@
                         source=source, locals=locals)
             code = compile(source, '<single>', 'single')
             if locals is not None:
-                exec code in self.globals, locals
+                exec(code, self.globals, locals)
             else:
-                exec code in self.globals
+                exec(code, self.globals)
             self.invoke('afterSingle')
         finally:
             self.pop()
@@ -2621,7 +2610,7 @@
                 hook.push()
                 try:
                     method = getattr(hook, _name)
-                    apply(method, (), keywords)
+                    method(*(), **keywords)
                 finally:
                     hook.pop()
 
@@ -2674,7 +2663,7 @@
             # installed it before ...
             if Interpreter._wasProxyInstalled:
                 # ... and if so, we have a proxy problem.
-                raise Error, "interpreter stdout proxy lost"
+                raise Error("interpreter stdout proxy lost")
             else:
                 # Otherwise, install the proxy and set the flag.
                 sys.stdout = ProxyFile(sys.stdout)
@@ -2780,7 +2769,7 @@
 
     def invokeHook(self, _name, **keywords):
         """Manually invoke a hook."""
-        apply(self.invoke, (_name,), keywords)
+        self.invoke(*(_name,), **keywords)
 
     # Callbacks.
 
@@ -2800,7 +2789,7 @@
         """Invoke the callback."""
         if self.callback is None:
             if self.options.get(CALLBACK_OPT, False):
-                raise Error, "custom markup invoked with no defined callback"
+                raise Error("custom markup invoked with no defined callback")
         else:
             self.callback(contents)
 
@@ -2810,7 +2799,7 @@
         """Flatten the contents of the pseudo-module into the globals
         namespace."""
         if keys is None:
-            keys = self.__dict__.keys() + self.__class__.__dict__.keys()
+            keys = list(self.__dict__.keys()) + list(self.__class__.__dict__.keys())
         dict = {}
         for key in keys:
             # The pseudomodule is really a class instance, so we need to
@@ -2879,8 +2868,7 @@
 
     def getAllDiversions(self):
         """Get the names of all existing diversions."""
-        names = self.stream().diversions.keys()
-        names.sort()
+        names = sorted(list(self.stream().diversions.keys()))
         return names
     
     # Filter.
@@ -2940,7 +2928,7 @@
         self.documents = {}
 
     def scan(self, basename, extensions=DEFAULT_EMPY_EXTENSIONS):
-        if type(extensions) is types.StringType:
+        if isinstance(extensions, bytes):
             extensions = (extensions,)
         def _noCriteria(x):
             return True
@@ -2988,7 +2976,7 @@
         match = self.SIGNIFICATOR_RE.search(line)
         if match:
             key, valueS = match.groups()
-            valueS = string.strip(valueS)
+            valueS = valueS.strip()
             if valueS:
                 value = eval(valueS)
             else:
@@ -3027,7 +3015,7 @@
     """Get data from the current environment.  If the default is True
     or False, then presume that we're only interested in the existence
     or non-existence of the environment variable."""
-    if os.environ.has_key(name):
+    if name in os.environ:
         # Do the True/False test by value for future compatibility.
         if default == False or default == True:
             return True
@@ -3110,7 +3098,7 @@
     _pauseAtEnd = False
     _relativePath = False
     if _extraArguments is not None:
-        _extraArguments = string.split(_extraArguments)
+        _extraArguments = _extraArguments.split()
         args = _extraArguments + args
     # Parse the arguments.
     pairs, remainder = getopt.getopt(args, 'VhHvkp:m:frino:a:buBP:I:D:E:F:', ['version', 'help', 'extended-help', 'verbose', 'null-hook', 'suppress-errors', 'prefix=', 'no-prefix', 'module=', 'flatten', 'raw-errors', 'interactive', 'no-override-stdout', 'binary', 'chunk-size=', 'output=' 'append=', 'preprocess=', 'import=', 'define=', 'execute=', 'execute-file=', 'buffered-output', 'pause-at-end', 'relative-path', 'no-callback-error', 'no-bangpath-processing', 'unicode', 'unicode-encoding=', 'unicode-input-encoding=', 'unicode-output-encoding=', 'unicode-errors=', 'unicode-input-errors=', 'unicode-output-errors='])
@@ -3160,8 +3148,8 @@
         elif option in ('-P', '--preprocess'):
             _preprocessing.append(('pre', argument))
         elif option in ('-I', '--import'):
-            for module in string.split(argument, ','):
-                module = string.strip(module)
+            for module in argument.split(','):
+                module = module.strip()
                 _preprocessing.append(('import', module))
         elif option in ('-D', '--define'):
             _preprocessing.append(('define', argument))
@@ -3200,18 +3188,18 @@
                                 _unicodeInputErrors, _unicodeOutputErrors)
     # Now initialize the output file if something has already been selected.
     if _output is not None:
-        _output = apply(AbstractFile, _output)
+        _output = AbstractFile(*_output)
     # Set up the main filename and the argument.
     if not remainder:
         remainder.append('-')
     filename, arguments = remainder[0], remainder[1:]
     # Set up the interpreter.
     if _options[BUFFERED_OPT] and _output is None:
-        raise ValueError, "-b only makes sense with -o or -a arguments"
+        raise ValueError("-b only makes sense with -o or -a arguments")
     if _prefix == 'None':
         _prefix = None
-    if _prefix and type(_prefix) is types.StringType and len(_prefix) != 1:
-        raise Error, "prefix must be single-character string"
+    if _prefix and isinstance(_prefix, bytes) and len(_prefix) != 1:
+        raise Error("prefix must be single-character string")
     interpreter = Interpreter(output=_output, \
                               argv=remainder, \
                               prefix=_prefix, \
@@ -3228,7 +3216,7 @@
                 name = thing
             elif which == 'define':
                 command = interpreter.string
-                if string.find(thing, '=') >= 0:
+                if thing.find('=') >= 0:
                     target = '%s{%s}' % (_prefix, thing)
                 else:
                     target = '%s{%s = None}' % (_prefix, thing)
@@ -3240,7 +3228,7 @@
             elif which == 'file':
                 command = interpreter.string
                 name = '<file:%d (%s)>' % (i, thing)
-                target = '%s{execfile("""%s""")}' % (_prefix, thing)
+                target = '%s{exec(open("""%s""").read())}' % (_prefix, thing)
             elif which == 'import':
                 command = interpreter.string
                 name = '<import:%d>' % i
@@ -3278,7 +3266,7 @@
     # Finally, if we should pause at the end, do it.
     if _pauseAtEnd:
         try:
-            raw_input()
+            input()
         except EOFError:
             pass
 
--- a/sample.em
+++ b/sample.em
@@ -19,7 +19,7 @@
 @{
 import sys
 # This is just a normal Python comment.
-print "This is more text."
+print("This is more text.")
 }@
 @# Note the @{ ... }@ convention to suppress the newline following the }.
 @# Also note that comments are completely tossed: This is not expanded: @(x).
@@ -40,11 +40,11 @@
 Statements: @{sys.stdout.write("%d**2 = %d" % (x, square(x)))}.
 Whitespace too: @{ sys.stdout.write("%d**2 = %d (still)" % (x, square(x))) }.
 @{
-print "But only on single-line statement expansions."
+print("But only on single-line statement expansions.")
 if 1:
-    print "Internal whitespace on multi-line statements is significant."
+    print("Internal whitespace on multi-line statements is significant.")
 for i in range(2):
-    print "Normal Python indentation rules must be followed here."
+    print("Normal Python indentation rules must be followed here.")
 }@
 Simple expressions: x is @x, l is @l, s is "@s," and @x squared is @square(x).
 Literals too: x is @x, but would be written @@x.
@@ -58,7 +58,7 @@
 Same with functions: @square(x) is @square( x ).
 The same applies to the other forms.
 Involved: The legal digits are @string.digits.
-More involved: The names of my friends are @string.join(friends, ', ').
+More involved: The names of my friends are @(', '.join(friends)).
 Following expressions: Pluralize "@word" as "@(word)s," or maybe "@word@ s."
 By default str is used (@s), but you can use repr if you want (@`s`).
 Conditional expressions: @(x ? "x is true" ! "x is false").
@@ -213,9 +213,9 @@
 @# Filters.
 @{import string}@
 This line should be in mixed case.
-@empy.setFilter(string.lower)@
+@empy.setFilter(str.lower)@
 This line should be all lowercase.
-@empy.setFilter(string.upper)@
+@empy.setFilter(str.upper)@
 This line should be all uppercase (how gauche).
 @empy.setFilter([em.LineBufferedFilter(), lambda x: '[%s]\n' % x[:-1]])@
 This line should be bracketed.
@@ -226,7 +226,7 @@
 This line should not appear at all!
 @empy.resetFilter()@
 This line should be back to mixed case.
-@empy.attachFilter(string.upper)@
+@empy.attachFilter(str.upper)@
 @empy.attachFilter(em.LineBufferedFilter())@
 @empy.attachFilter(lambda x: '[%s]\n' % x[:-1])@
 This line should be all uppercase with brackets.
@@ -235,10 +235,10 @@
 
 @# Contexts, metaoperations.
 @{
-import StringIO
-def context():
-    return "%s:%d" % empy.identify()
-stringFile = StringIO.StringIO("2 + 2 = @(2 + 2) [@context()].\n")
+from __future__ import unicode_literals
+from io import StringIO
+def context(): return "%s:%d" % empy.identify()
+stringFile = StringIO("2 + 2 = @(2 + 2) [@context()].\n")
 }@
 The new context is @context().
 File inclusion [@context()]: @empy.include(stringFile)@
--- a/test.sh
+++ b/test.sh
@@ -13,7 +13,7 @@
 SAMPLE=sample.em
 BENCH=sample.bench
 
-if $PYTHON -c 'import sys; print sys.version' > /dev/null
+if $PYTHON -c 'import sys; print(sys.version)' > /dev/null
 then
     :
 else
