diff --git a/CHANGES b/CHANGES
index f048aed413a79dd7f07d5afe91934d422671bcd3..d3ed5ea0180d251794779b82044048aeb75254f4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,9 @@
 0.3
 - Python 2.3 support is dropped. [ticket:123]
+
+- Python 3 support is added ! See README.py3k
+  for installation and testing notes.
+  [ticket:119]
   
 - Unit tests now run with nose.  [ticket:127]
 
diff --git a/README.py3k b/README.py3k
new file mode 100644
index 0000000000000000000000000000000000000000..3e3c8ab8b5c2fd0596a8133181bb39ea09ad008c
--- /dev/null
+++ b/README.py3k
@@ -0,0 +1,50 @@
+=================
+PYTHON 3 SUPPORT
+=================
+
+Python 3 support in Mako is provided by the Python 2to3 script.
+
+Installing Distribute
+---------------------
+
+Distribute should be installed with the Python3 installation.  The
+distribute bootloader is included.
+
+Running as a user with permission to modify the Python distribution,
+install Distribute:
+
+    python3 distribute_setup.py
+    
+
+Installing Mako in Python 3
+---------------------------------
+
+Once Distribute is installed, Mako can be installed directly.  
+The 2to3 process will kick in which takes several minutes:
+
+    python3 setup.py install
+
+Converting Tests, Examples, Source to Python 3
+----------------------------------------------
+
+To convert all files in the source distribution, run 
+the 2to3 script:
+
+    2to3 --no-diffs -w lib test examples
+
+The above will rewrite all files in-place in Python 3 format.
+
+Running Tests
+-------------
+
+To run the unit tests, ensure Distribute is installed as above,
+and also that at least the ./lib/ and ./test/ directories have been converted
+to Python 3 using the source tool above.   A Python 3 version of Nose
+can be acquired from Bitbucket using Mercurial:
+
+    hg clone http://bitbucket.org/jpellerin/nose3/
+    cd nose3
+    python3 setup.py install
+
+The tests can then be run using the "nosetests3" script installed by the above.
+
diff --git a/distribute_setup.py b/distribute_setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..002133624dcc3950b99276e135b3402853596fd2
--- /dev/null
+++ b/distribute_setup.py
@@ -0,0 +1,477 @@
+#!python
+"""Bootstrap distribute installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from distribute_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import os
+import sys
+import time
+import fnmatch
+import tempfile
+import tarfile
+from distutils import log
+
+try:
+    from site import USER_SITE
+except ImportError:
+    USER_SITE = None
+
+try:
+    import subprocess
+
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        return subprocess.call(args) == 0
+
+except ImportError:
+    # will be used for python 2.3
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        # quoting arguments if windows
+        if sys.platform == 'win32':
+            def quote(arg):
+                if ' ' in arg:
+                    return '"%s"' % arg
+                return arg
+            args = [quote(arg) for arg in args]
+        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
+
+DEFAULT_VERSION = "0.6.10"
+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
+SETUPTOOLS_FAKED_VERSION = "0.6c11"
+
+SETUPTOOLS_PKG_INFO = """\
+Metadata-Version: 1.0
+Name: setuptools
+Version: %s
+Summary: xxxx
+Home-page: xxx
+Author: xxx
+Author-email: xxx
+License: xxx
+Description: xxx
+""" % SETUPTOOLS_FAKED_VERSION
+
+
+def _install(tarball):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # installing
+        log.warn('Installing Distribute')
+        if not _python_cmd('setup.py', 'install'):
+            log.warn('Something went wrong during the installation.')
+            log.warn('See the error message above.')
+    finally:
+        os.chdir(old_wd)
+
+
+def _build_egg(egg, tarball, to_dir):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # building an egg
+        log.warn('Building a Distribute egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+
+    finally:
+        os.chdir(old_wd)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+def _do_download(version, download_base, to_dir, download_delay):
+    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
+                       % (version, sys.version_info[0], sys.version_info[1]))
+    if not os.path.exists(egg):
+        tarball = download_setuptools(version, download_base,
+                                      to_dir, download_delay)
+        _build_egg(egg, tarball, to_dir)
+    sys.path.insert(0, egg)
+    import setuptools
+    setuptools.bootstrap_install_from = egg
+
+
+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                   to_dir=os.curdir, download_delay=15, no_fake=True):
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    was_imported = 'pkg_resources' in sys.modules or \
+        'setuptools' in sys.modules
+    try:
+        try:
+            import pkg_resources
+            if not hasattr(pkg_resources, '_distribute'):
+                if not no_fake:
+                    _fake_setuptools()
+                raise ImportError
+        except ImportError:
+            return _do_download(version, download_base, to_dir, download_delay)
+        try:
+            pkg_resources.require("distribute>="+version)
+            return
+        except pkg_resources.VersionConflict:
+            e = sys.exc_info()[1]
+            if was_imported:
+                sys.stderr.write(
+                "The required version of distribute (>=%s) is not available,\n"
+                "and can't be installed while this script is running. Please\n"
+                "install a more recent version first, using\n"
+                "'easy_install -U distribute'."
+                "\n\n(Currently using %r)\n" % (version, e.args[0]))
+                sys.exit(2)
+            else:
+                del pkg_resources, sys.modules['pkg_resources']    # reload ok
+                return _do_download(version, download_base, to_dir,
+                                    download_delay)
+        except pkg_resources.DistributionNotFound:
+            return _do_download(version, download_base, to_dir,
+                                download_delay)
+    finally:
+        if not no_fake:
+            _create_fake_setuptools_pkg_info(to_dir)
+
+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                        to_dir=os.curdir, delay=15):
+    """Download distribute from a specified location and return its filename
+
+    `version` should be a valid distribute version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download
+    attempt.
+    """
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+    tgz_name = "distribute-%s.tar.gz" % version
+    url = download_base + tgz_name
+    saveto = os.path.join(to_dir, tgz_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            log.warn("Downloading %s", url)
+            src = urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = src.read()
+            dst = open(saveto, "wb")
+            dst.write(data)
+        finally:
+            if src:
+                src.close()
+            if dst:
+                dst.close()
+    return os.path.realpath(saveto)
+
+
+def _patch_file(path, content):
+    """Will backup the file then patch it"""
+    existing_content = open(path).read()
+    if existing_content == content:
+        # already patched
+        log.warn('Already patched.')
+        return False
+    log.warn('Patching...')
+    _rename_path(path)
+    f = open(path, 'w')
+    try:
+        f.write(content)
+    finally:
+        f.close()
+    return True
+
+
+def _same_content(path, content):
+    return open(path).read() == content
+
+def _no_sandbox(function):
+    def __no_sandbox(*args, **kw):
+        try:
+            from setuptools.sandbox import DirectorySandbox
+            def violation(*args):
+                pass
+            DirectorySandbox._old = DirectorySandbox._violation
+            DirectorySandbox._violation = violation
+            patched = True
+        except ImportError:
+            patched = False
+
+        try:
+            return function(*args, **kw)
+        finally:
+            if patched:
+                DirectorySandbox._violation = DirectorySandbox._old
+                del DirectorySandbox._old
+
+    return __no_sandbox
+
+@_no_sandbox
+def _rename_path(path):
+    new_name = path + '.OLD.%s' % time.time()
+    log.warn('Renaming %s into %s', path, new_name)
+    os.rename(path, new_name)
+    return new_name
+
+def _remove_flat_installation(placeholder):
+    if not os.path.isdir(placeholder):
+        log.warn('Unkown installation at %s', placeholder)
+        return False
+    found = False
+    for file in os.listdir(placeholder):
+        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
+            found = True
+            break
+    if not found:
+        log.warn('Could not locate setuptools*.egg-info')
+        return
+
+    log.warn('Removing elements out of the way...')
+    pkg_info = os.path.join(placeholder, file)
+    if os.path.isdir(pkg_info):
+        patched = _patch_egg_dir(pkg_info)
+    else:
+        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
+
+    if not patched:
+        log.warn('%s already patched.', pkg_info)
+        return False
+    # now let's move the files out of the way
+    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
+        element = os.path.join(placeholder, element)
+        if os.path.exists(element):
+            _rename_path(element)
+        else:
+            log.warn('Could not find the %s element of the '
+                     'Setuptools distribution', element)
+    return True
+
+
+def _after_install(dist):
+    log.warn('After install bootstrap.')
+    placeholder = dist.get_command_obj('install').install_purelib
+    _create_fake_setuptools_pkg_info(placeholder)
+
+@_no_sandbox
+def _create_fake_setuptools_pkg_info(placeholder):
+    if not placeholder or not os.path.exists(placeholder):
+        log.warn('Could not find the install location')
+        return
+    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
+    setuptools_file = 'setuptools-%s-py%s.egg-info' % \
+            (SETUPTOOLS_FAKED_VERSION, pyver)
+    pkg_info = os.path.join(placeholder, setuptools_file)
+    if os.path.exists(pkg_info):
+        log.warn('%s already exists', pkg_info)
+        return
+
+    log.warn('Creating %s', pkg_info)
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+
+    pth_file = os.path.join(placeholder, 'setuptools.pth')
+    log.warn('Creating %s', pth_file)
+    f = open(pth_file, 'w')
+    try:
+        f.write(os.path.join(os.curdir, setuptools_file))
+    finally:
+        f.close()
+
+def _patch_egg_dir(path):
+    # let's check if it's already patched
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    if os.path.exists(pkg_info):
+        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
+            log.warn('%s already patched.', pkg_info)
+            return False
+    _rename_path(path)
+    os.mkdir(path)
+    os.mkdir(os.path.join(path, 'EGG-INFO'))
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+    return True
+
+
+def _before_install():
+    log.warn('Before install bootstrap.')
+    _fake_setuptools()
+
+
+def _under_prefix(location):
+    if 'install' not in sys.argv:
+        return True
+    args = sys.argv[sys.argv.index('install')+1:]
+    for index, arg in enumerate(args):
+        for option in ('--root', '--prefix'):
+            if arg.startswith('%s=' % option):
+                top_dir = arg.split('root=')[-1]
+                return location.startswith(top_dir)
+            elif arg == option:
+                if len(args) > index:
+                    top_dir = args[index+1]
+                    return location.startswith(top_dir)
+            elif option == '--user' and USER_SITE is not None:
+                return location.startswith(USER_SITE)
+    return True
+
+
+def _fake_setuptools():
+    log.warn('Scanning installed packages')
+    try:
+        import pkg_resources
+    except ImportError:
+        # we're cool
+        log.warn('Setuptools or Distribute does not seem to be installed.')
+        return
+    ws = pkg_resources.working_set
+    try:
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
+                                  replacement=False))
+    except TypeError:
+        # old distribute API
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
+
+    if setuptools_dist is None:
+        log.warn('No setuptools distribution found')
+        return
+    # detecting if it was already faked
+    setuptools_location = setuptools_dist.location
+    log.warn('Setuptools installation detected at %s', setuptools_location)
+
+    # if --root or --preix was provided, and if
+    # setuptools is not located in them, we don't patch it
+    if not _under_prefix(setuptools_location):
+        log.warn('Not patching, --root or --prefix is installing Distribute'
+                 ' in another location')
+        return
+
+    # let's see if its an egg
+    if not setuptools_location.endswith('.egg'):
+        log.warn('Non-egg installation')
+        res = _remove_flat_installation(setuptools_location)
+        if not res:
+            return
+    else:
+        log.warn('Egg installation')
+        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
+        if (os.path.exists(pkg_info) and
+            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
+            log.warn('Already patched.')
+            return
+        log.warn('Patching...')
+        # let's create a fake egg replacing setuptools one
+        res = _patch_egg_dir(setuptools_location)
+        if not res:
+            return
+    log.warn('Patched done.')
+    _relaunch()
+
+
+def _relaunch():
+    log.warn('Relaunching...')
+    # we have to relaunch the process
+    args = [sys.executable] + sys.argv
+    sys.exit(subprocess.call(args))
+
+
+def _extractall(self, path=".", members=None):
+    """Extract all members from the archive to the current working
+       directory and set owner, modification time and permissions on
+       directories afterwards. `path' specifies a different directory
+       to extract to. `members' is optional and must be a subset of the
+       list returned by getmembers().
+    """
+    import copy
+    import operator
+    from tarfile import ExtractError
+    directories = []
+
+    if members is None:
+        members = self
+
+    for tarinfo in members:
+        if tarinfo.isdir():
+            # Extract directories with a safe mode.
+            directories.append(tarinfo)
+            tarinfo = copy.copy(tarinfo)
+            tarinfo.mode = 448 # decimal for oct 0700
+        self.extract(tarinfo, path)
+
+    # Reverse sort directories.
+    if sys.version_info < (2, 4):
+        def sorter(dir1, dir2):
+            return cmp(dir1.name, dir2.name)
+        directories.sort(sorter)
+        directories.reverse()
+    else:
+        directories.sort(key=operator.attrgetter('name'), reverse=True)
+
+    # Set correct owner, mtime and filemode on directories.
+    for tarinfo in directories:
+        dirpath = os.path.join(path, tarinfo.name)
+        try:
+            self.chown(tarinfo, dirpath)
+            self.utime(tarinfo, dirpath)
+            self.chmod(tarinfo, dirpath)
+        except ExtractError:
+            e = sys.exc_info()[1]
+            if self.errorlevel > 1:
+                raise
+            else:
+                self._dbg(1, "tarfile: %s" % e)
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    tarball = download_setuptools()
+    _install(tarball)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
diff --git a/mako/ast.py b/mako/ast.py
index 8d5b1d768faac3ff298d6e078b75da764a21aa3e..242b6eecda01187c423684817ad21a02cea3f50c 100644
--- a/mako/ast.py
+++ b/mako/ast.py
@@ -4,7 +4,8 @@
 # This module is part of Mako and is released under
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
-"""utilities for analyzing expressions and blocks of Python code, as well as generating Python from AST nodes"""
+"""utilities for analyzing expressions and blocks of Python 
+code, as well as generating Python from AST nodes"""
 
 from mako import exceptions, pyparser, util
 import re
@@ -22,9 +23,12 @@ class PythonCode(object):
         
         # note that an identifier can be in both the undeclared and declared lists.
 
-        # using AST to parse instead of using code.co_varnames, code.co_names has several advantages:
-        # - we can locate an identifier as "undeclared" even if its declared later in the same block of code
-        # - AST is less likely to break with version changes (for example, the behavior of co_names changed a little bit
+        # using AST to parse instead of using code.co_varnames, 
+        # code.co_names has several advantages:
+        # - we can locate an identifier as "undeclared" even if 
+        # its declared later in the same block of code
+        # - AST is less likely to break with version changes 
+        # (for example, the behavior of co_names changed a little bit
         # in python version 2.5)
         if isinstance(code, basestring):
             expr = pyparser.parse(code.lstrip(), "exec", **exception_kwargs)
@@ -65,7 +69,9 @@ class PythonFragment(PythonCode):
     def __init__(self, code, **exception_kwargs):
         m = re.match(r'^(\w+)(?:\s+(.*?))?:\s*(#|$)', code.strip(), re.S)
         if not m:
-            raise exceptions.CompileException("Fragment '%s' is not a partial control statement" % code, **exception_kwargs)
+            raise exceptions.CompileException(
+                            "Fragment '%s' is not a partial control statement" % 
+                            code, **exception_kwargs)
         if m.group(3):
             code = code[:m.start(3)]
         (keyword, expr) = m.group(1,2)
@@ -78,7 +84,9 @@ class PythonFragment(PythonCode):
         elif keyword == 'except':
             code = "try:pass\n" + code + "pass"
         else:
-            raise exceptions.CompileException("Unsupported control keyword: '%s'" % keyword, **exception_kwargs)
+            raise exceptions.CompileException(
+                                "Unsupported control keyword: '%s'" % 
+                                keyword, **exception_kwargs)
         super(PythonFragment, self).__init__(code, **exception_kwargs)
         
         
@@ -91,12 +99,17 @@ class FunctionDecl(object):
         f = pyparser.ParseFunc(self, **exception_kwargs)
         f.visit(expr)
         if not hasattr(self, 'funcname'):
-            raise exceptions.CompileException("Code '%s' is not a function declaration" % code, **exception_kwargs)
+            raise exceptions.CompileException(
+                                "Code '%s' is not a function declaration" % code,
+                                **exception_kwargs)
         if not allow_kwargs and self.kwargs:
-            raise exceptions.CompileException("'**%s' keyword argument not allowed here" % self.argnames[-1], **exception_kwargs)
+            raise exceptions.CompileException(
+                                "'**%s' keyword argument not allowed here" % 
+                                self.argnames[-1], **exception_kwargs)
             
     def get_argument_expressions(self, include_defaults=True):
         """return the argument declarations of this FunctionDecl as a printable list."""
+        
         namedecls = []
         defaults = [d for d in self.defaults]
         kwargs = self.kwargs
@@ -114,12 +127,17 @@ class FunctionDecl(object):
             else:
                 default = len(defaults) and defaults.pop() or None
             if include_defaults and default:
-                namedecls.insert(0, "%s=%s" % (arg, pyparser.ExpressionGenerator(default).value()))
+                namedecls.insert(0, "%s=%s" % 
+                            (arg, 
+                            pyparser.ExpressionGenerator(default).value()
+                            )
+                        )
             else:
                 namedecls.insert(0, arg)
         return namedecls
 
 class FunctionArgs(FunctionDecl):
     """the argument portion of a function declaration"""
+    
     def __init__(self, code, **kwargs):
         super(FunctionArgs, self).__init__("def ANON(%s):pass" % code, **kwargs)
diff --git a/mako/codegen.py b/mako/codegen.py
index 41ab7fecb6085c3da516877dfbb16ab9418aa827..ded93960a90428a663ae6188d1fb0236cb0c1a3a 100644
--- a/mako/codegen.py
+++ b/mako/codegen.py
@@ -25,6 +25,14 @@ def compile(node,
     """Generate module source code given a parsetree node, 
       uri, and optional source filename"""
 
+    # if on Py2K, push the "source_encoding" string to be
+    # a bytestring itself, as we will be embedding it into 
+    # the generated source and we don't want to coerce the 
+    # result into a unicode object, in "disable_unicode" mode
+    if not util.py3k and isinstance(source_encoding, unicode):
+        source_encoding = source_encoding.encode(source_encoding)
+        
+        
     buf = util.FastEncodingBuffer()
 
     printer = PythonPrinter(buf)
@@ -571,8 +579,9 @@ class _GenerateRenderMethod(object):
             if not self.in_def and len(self.identifiers.locally_assigned) > 0:
                 # if we are the "template" def, fudge locally declared/modified variables into the "__M_locals" dictionary,
                 # which is used for def calls within the same template, to simulate "enclosing scope"
-                self.printer.writeline('__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin()[__M_key]) for __M_key in [%s] if __M_key in __M_locals_builtin()]))' % ','.join([repr(x) for x in node.declared_identifiers()]))
-                
+                self.printer.writeline('__M_locals_builtin_stored = __M_locals_builtin()')
+                self.printer.writeline('__M_locals.update(__M_dict_builtin([(__M_key, __M_locals_builtin_stored[__M_key]) for __M_key in [%s] if __M_key in __M_locals_builtin_stored]))' % ','.join([repr(x) for x in node.declared_identifiers()]))
+
     def visitIncludeTag(self, node):
         self.write_source_comment(node)
         args = node.attributes.get('args')
diff --git a/mako/exceptions.py b/mako/exceptions.py
index dcd6a644c4445039006dc26de2bdaf328efc5cc5..b8b7747e7722a81d097a12ef1942264bfc942cab 100644
--- a/mako/exceptions.py
+++ b/mako/exceptions.py
@@ -19,7 +19,9 @@ def _format_filepos(lineno, pos, filename):
     if filename is None:
         return " at line: %d char: %d" % (lineno, pos)
     else:
-        return " in file '%s' at line: %d char: %d" % (filename, lineno, pos)     
+        return " in file '%s' at line: %d char: %d" % (filename, lineno, pos)
+        
+        
 class CompileException(MakoException):
     def __init__(self, message, source, lineno, pos, filename):
         MakoException.__init__(self, message + _format_filepos(lineno, pos, filename))
@@ -35,6 +37,9 @@ class SyntaxException(MakoException):
         self.pos = pos
         self.filename = filename
         self.source = source
+
+class UnsupportedError(MakoException):
+    """raised when a retired feature is used."""
         
 class TemplateLookupException(MakoException):
     pass
@@ -130,17 +135,19 @@ class RichTraceback(object):
                     template_filename = info.template_filename or filename
                 except KeyError:
                     # A normal .py file (not a Template)
-                    try:
-                        fp = open(filename)
-                        encoding = util.parse_encoding(fp)
-                        fp.close()
-                    except IOError:
-                        encoding = None
-                    if encoding:
-                        line = line.decode(encoding)
-                    else:
-                        line = line.decode('ascii', 'replace')
-                    new_trcback.append((filename, lineno, function, line, None, None, None, None))
+                    if not util.py3k:
+                        try:
+                            fp = open(filename, 'rb')
+                            encoding = util.parse_encoding(fp)
+                            fp.close()
+                        except IOError:
+                            encoding = None
+                        if encoding:
+                            line = line.decode(encoding)
+                        else:
+                            line = line.decode('ascii', 'replace')
+                    new_trcback.append((filename, lineno, function, line, 
+                                            None, None, None, None))
                     continue
 
                 template_ln = module_ln = 1
@@ -161,7 +168,9 @@ class RichTraceback(object):
                 template_line = template_lines[template_ln - 1]
             else:
                 template_line = None
-            new_trcback.append((filename, lineno, function, line, template_filename, template_ln, template_line, template_source))
+            new_trcback.append((filename, lineno, function, 
+                                line, template_filename, template_ln, 
+                                template_line, template_source))
         if not self.source:
             for l in range(len(new_trcback)-1, 0, -1):
                 if new_trcback[l][5]:
@@ -171,7 +180,7 @@ class RichTraceback(object):
             else:
                 try:
                     # A normal .py file (not a Template)
-                    fp = open(new_trcback[-1][0])
+                    fp = open(new_trcback[-1][0], 'rb')
                     encoding = util.parse_encoding(fp)
                     fp.seek(0)
                     self.source = fp.read()
diff --git a/mako/filters.py b/mako/filters.py
index 9a5b21dda473fc15252bfa306cb3b3be1c69e832..5f35714b13250745ec7fa34f2d115065e15e0d7d 100644
--- a/mako/filters.py
+++ b/mako/filters.py
@@ -7,6 +7,7 @@
 
 import re, cgi, urllib, htmlentitydefs, codecs
 from StringIO import StringIO
+from mako import util
 
 xml_escapes = {
     '&' : '&amp;',
@@ -166,5 +167,9 @@ DEFAULT_ESCAPES = {
     'str':'str',
     'n':'n'
 }
-    
+
+if util.py3k:
+    DEFAULT_ESCAPES.update({
+        'unicode':'str'
+    })
 
diff --git a/mako/lexer.py b/mako/lexer.py
index caf295b8309abf38eaeb1bda628bf8283491eea9..5e4a3bcca34ebdfbc7180e8b317f5b12a4f6f379 100644
--- a/mako/lexer.py
+++ b/mako/lexer.py
@@ -7,7 +7,7 @@
 """provides the Lexer class for parsing template strings into parse trees."""
 
 import re, codecs
-from mako import parsetree, exceptions
+from mako import parsetree, exceptions, util
 from mako.pygen import adjust_whitespace
 
 _regexp_cache = {}
@@ -27,6 +27,12 @@ class Lexer(object):
         self.control_line = []
         self.disable_unicode = disable_unicode
         self.encoding = input_encoding
+        
+        if util.py3k and disable_unicode:
+            raise exceptions.UnsupportedError(
+                                    "Mako for Python 3 does not "
+                                    "support disabling Unicode")
+        
         if preprocessor is None:
             self.preprocessor = []
         elif not hasattr(preprocessor, '__iter__'):
@@ -42,10 +48,8 @@ class Lexer(object):
                 'filename':self.filename}
     
     def match(self, regexp, flags=None):
-        """match the given regular expression string and flags to the current text position.
+        """compile the given regexp, cache the reg, and call match_reg()."""
         
-        if a match occurs, update the current text and line position."""
-        mp = self.match_position
         try:
             reg = _regexp_cache[(regexp, flags)]
         except KeyError:
@@ -54,6 +58,17 @@ class Lexer(object):
             else:
                 reg = re.compile(regexp)
             _regexp_cache[(regexp, flags)] = reg
+        
+        return self.match_reg(reg)
+        
+    def match_reg(self, reg):
+        """match the given regular expression object to the current text position.
+        
+        if a match occurs, update the current text and line position.
+        
+        """
+
+        mp = self.match_position
 
         match = reg.match(self.text, self.match_position)
         if match:
@@ -128,45 +143,61 @@ class Lexer(object):
                                 (node.keyword, self.control_line[-1].keyword),
                                 **self.exception_kwargs)
 
-    def parse(self):
-        for preproc in self.preprocessor:
-            self.text = preproc(self.text)
-            
-        if not isinstance(self.text, unicode) and self.text.startswith(codecs.BOM_UTF8):
-            self.text = self.text[len(codecs.BOM_UTF8):]
+    _coding_re = re.compile(r'#.*coding[:=]\s*([-\w.]+).*\r?\n')
+
+    def decode_raw_stream(self, text, decode_raw, known_encoding, filename):
+        """given string/unicode or bytes/string, determine encoding
+           from magic encoding comment, return body as unicode
+           or raw if decode_raw=False
+
+        """
+        if isinstance(text, unicode):
+            m = self._coding_re.match(text)
+            encoding = m and m.group(1) or known_encoding or 'ascii'
+            return encoding, text
+
+        if text.startswith(codecs.BOM_UTF8):
+            text = text[len(codecs.BOM_UTF8):]
             parsed_encoding = 'utf-8'
-            me = self.match_encoding()
-            if me is not None and me != 'utf-8':
+            m = self._coding_re.match(text.decode('utf-8', 'ignore'))
+            if m is not None and m.group(1) != 'utf-8':
                 raise exceptions.CompileException(
                                 "Found utf-8 BOM in file, with conflicting "
-                                "magic encoding comment of '%s'" % me, 
-                                self.text.decode('utf-8', 'ignore'), 
-                                0, 0, self.filename)
+                                "magic encoding comment of '%s'" % m.group(1), 
+                                text.decode('utf-8', 'ignore'), 
+                                0, 0, filename)
         else:
-            parsed_encoding = self.match_encoding()
-            
-        if parsed_encoding:
-            self.encoding = parsed_encoding
-            
-        if not self.disable_unicode and not isinstance(self.text, unicode):
-            if self.encoding:
-                try:
-                    self.text = self.text.decode(self.encoding)
-                except UnicodeDecodeError, e:
-                    raise exceptions.CompileException(
-                                    "Unicode decode operation of encoding '%s' failed" %
-                                    self.encoding, 
-                                    self.text.decode('utf-8', 'ignore'), 
-                                    0, 0, self.filename)
+            m = self._coding_re.match(text.decode('utf-8', 'ignore'))
+            if m:
+                parsed_encoding = m.group(1)
             else:
-                try:
-                    self.text = self.text.decode()
-                except UnicodeDecodeError, e:
-                    raise exceptions.CompileException(
-                                    "Could not read template using encoding of 'ascii'.  "
-                                    "Did you forget a magic encoding comment?",
-                                    self.text.decode('utf-8', 'ignore'), 0, 0, self.filename)
+                parsed_encoding = known_encoding or 'ascii'
+
+        if decode_raw:
+            try:
+                text = text.decode(parsed_encoding)
+            except UnicodeDecodeError, e:
+                raise exceptions.CompileException(
+                                "Unicode decode operation of encoding '%s' failed" %
+                                parsed_encoding, 
+                                text.decode('utf-8', 'ignore'), 
+                                0, 0, filename)
+
+        return parsed_encoding, text
+
+    def parse(self):
+        self.encoding, self.text = self.decode_raw_stream(self.text, 
+                                        not self.disable_unicode, 
+                                        self.encoding,
+                                        self.filename,)
 
+        for preproc in self.preprocessor:
+            self.text = preproc(self.text)
+        
+        # push the match marker past the 
+        # encoding comment.
+        self.match_reg(self._coding_re)
+        
         self.textlength = len(self.text)
             
         while (True):
@@ -206,13 +237,6 @@ class Lexer(object):
                                             self.control_line[-1].pos, self.filename)
         return self.template
 
-    def match_encoding(self):
-        match = self.match(r'#.*coding[:=]\s*([-\w.]+).*\r?\n')
-        if match:
-            return match.group(1)
-        else:
-            return None
-            
     def match_tag_start(self):
         match = self.match(r'''
             \<%     # opening tag
diff --git a/mako/parsetree.py b/mako/parsetree.py
index 48e2d106d2b0abf9fb46c3be9b9b0a2705cd5e76..3a273ac99b29735148660a98d13f4537550e3e04 100644
--- a/mako/parsetree.py
+++ b/mako/parsetree.py
@@ -235,11 +235,13 @@ class Tag(Node):
         
         attributes - raw dictionary of attribute key/value pairs
         
-        expressions - a set of identifiers that are legal attributes, which can also contain embedded expressions
+        expressions - a set of identifiers that are legal attributes, 
+            which can also contain embedded expressions
         
-        nonexpressions - a set of identifiers that are legal attributes, which cannot contain embedded expressions
+        nonexpressions - a set of identifiers that are legal attributes, 
+            which cannot contain embedded expressions
         
-        **kwargs - other arguments passed to the Node superclass (lineno, pos)
+        \**kwargs - other arguments passed to the Node superclass (lineno, pos)
         
         """
         super(Tag, self).__init__(**kwargs)
@@ -270,7 +272,9 @@ class Tag(Node):
                     m = re.match(r'^\${(.+?)}$', x)
                     if m:
                         code = ast.PythonCode(m.group(1), **self.exception_kwargs)
-                        undeclared_identifiers = undeclared_identifiers.union(code.undeclared_identifiers)
+                        undeclared_identifiers = undeclared_identifiers.union(
+                                                        code.undeclared_identifiers
+                                                    )
                         expr.append("(%s)" % m.group(1))
                     else:
                         if x:
@@ -279,11 +283,15 @@ class Tag(Node):
             elif key in nonexpressions:
                 if re.search(r'${.+?}', self.attributes[key]):
                     raise exceptions.CompileException(
-                            "Attibute '%s' in tag '%s' does not allow embedded expressions"  % (key, self.keyword), 
+                            "Attibute '%s' in tag '%s' does not allow embedded "
+                            "expressions"  % (key, self.keyword), 
                             **self.exception_kwargs)
                 self.parsed_attributes[key] = repr(self.attributes[key])
             else:
-                raise exceptions.CompileException("Invalid attribute for tag '%s': '%s'" %(self.keyword, key), **self.exception_kwargs)
+                raise exceptions.CompileException(
+                                    "Invalid attribute for tag '%s': '%s'" %
+                                    (self.keyword, key), 
+                                    **self.exception_kwargs)
         self.expression_undeclared_identifiers = undeclared_identifiers
 
     def declared_identifiers(self):
@@ -297,15 +305,21 @@ class Tag(Node):
                                         self.keyword, 
                                         util.sorted_dict_repr(self.attributes),
                                         (self.lineno, self.pos), 
-                                        [repr(x) for x in self.nodes]
+                                        self.nodes
                                     )
         
 class IncludeTag(Tag):
     __keyword__ = 'include'
 
     def __init__(self, keyword, attributes, **kwargs):
-        super(IncludeTag, self).__init__(keyword, attributes, ('file', 'import', 'args'), (), ('file',), **kwargs)
-        self.page_args = ast.PythonCode("__DUMMY(%s)" % attributes.get('args', ''), **self.exception_kwargs)
+        super(IncludeTag, self).__init__(
+                                    keyword, 
+                                    attributes, 
+                                    ('file', 'import', 'args'), 
+                                    (), ('file',), **kwargs)
+        self.page_args = ast.PythonCode(
+                                "__DUMMY(%s)" % attributes.get('args', ''),
+                                 **self.exception_kwargs)
 
     def declared_identifiers(self):
         return []
@@ -318,10 +332,19 @@ class NamespaceTag(Tag):
     __keyword__ = 'namespace'
 
     def __init__(self, keyword, attributes, **kwargs):
-        super(NamespaceTag, self).__init__(keyword, attributes, (), ('name','inheritable','file','import','module'), (), **kwargs)
+        super(NamespaceTag, self).__init__(
+                                        keyword, attributes, 
+                                        (), 
+                                        ('name','inheritable',
+                                        'file','import','module'), 
+                                        (), **kwargs)
+                                        
         self.name = attributes.get('name', '__anon_%s' % hex(abs(id(self))))
         if not 'name' in attributes and not 'import' in attributes:
-            raise exceptions.CompileException("'name' and/or 'import' attributes are required for <%namespace>", **self.exception_kwargs)
+            raise exceptions.CompileException(
+                                "'name' and/or 'import' attributes are required "
+                                "for <%namespace>", 
+                                **self.exception_kwargs)
 
     def declared_identifiers(self):
         return []
@@ -330,8 +353,13 @@ class TextTag(Tag):
     __keyword__ = 'text'
 
     def __init__(self, keyword, attributes, **kwargs):
-        super(TextTag, self).__init__(keyword, attributes, (), ('filter'), (), **kwargs)
-        self.filter_args = ast.ArgumentList(attributes.get('filter', ''), **self.exception_kwargs)
+        super(TextTag, self).__init__(
+                                    keyword, 
+                                    attributes, (), 
+                                    ('filter'), (), **kwargs)
+        self.filter_args = ast.ArgumentList(
+                                    attributes.get('filter', ''), 
+                                    **self.exception_kwargs)
         
 class DefTag(Tag):
     __keyword__ = 'def'
@@ -340,17 +368,22 @@ class DefTag(Tag):
         super(DefTag, self).__init__(
                 keyword, 
                 attributes, 
-                ('buffered', 'cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'cache_url'), 
+                ('buffered', 'cached', 'cache_key', 'cache_timeout', 
+                    'cache_type', 'cache_dir', 'cache_url'), 
                 ('name','filter', 'decorator'), 
                 ('name',), 
                 **kwargs)
         name = attributes['name']
         if re.match(r'^[\w_]+$',name):
-            raise exceptions.CompileException("Missing parenthesis in %def", **self.exception_kwargs)
+            raise exceptions.CompileException(
+                                "Missing parenthesis in %def", 
+                                **self.exception_kwargs)
         self.function_decl = ast.FunctionDecl("def " + name + ":pass", **self.exception_kwargs)
         self.name = self.function_decl.funcname
         self.decorator = attributes.get('decorator', '')
-        self.filter_args = ast.ArgumentList(attributes.get('filter', ''), **self.exception_kwargs)
+        self.filter_args = ast.ArgumentList(
+                                attributes.get('filter', ''), 
+                                **self.exception_kwargs)
 
     def declared_identifiers(self):
         return self.function_decl.argnames
@@ -359,13 +392,17 @@ class DefTag(Tag):
         res = []
         for c in self.function_decl.defaults:
             res += list(ast.PythonCode(c, **self.exception_kwargs).undeclared_identifiers)
-        return res + list(self.filter_args.undeclared_identifiers.difference(set(filters.DEFAULT_ESCAPES.keys())))
+        return res + list(self.filter_args.\
+                            undeclared_identifiers.\
+                            difference(filters.DEFAULT_ESCAPES.keys())
+                        )
 
 class CallTag(Tag):
     __keyword__ = 'call'
 
     def __init__(self, keyword, attributes, **kwargs):
-        super(CallTag, self).__init__(keyword, attributes, ('args'), ('expr',), ('expr',), **kwargs)
+        super(CallTag, self).__init__(keyword, attributes, 
+                                    ('args'), ('expr',), ('expr',), **kwargs)
         self.expression = attributes['expr']
         self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
         self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs)
@@ -386,9 +423,18 @@ class CallNamespaceTag(Tag):
                     (), 
                     (), 
                     **kwargs)
-        self.expression = "%s.%s(%s)" % (namespace, defname, ",".join(["%s=%s" % (k, v) for k, v in self.parsed_attributes.iteritems() if k != 'args']))
+                    
+        self.expression = "%s.%s(%s)" % (
+                                namespace, 
+                                defname, 
+                                ",".join(["%s=%s" % (k, v) for k, v in
+                                            self.parsed_attributes.iteritems() 
+                                            if k != 'args'])
+                            )
         self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
-        self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs)
+        self.body_decl = ast.FunctionArgs(
+                                    attributes.get('args', ''), 
+                                    **self.exception_kwargs)
 
     def declared_identifiers(self):
         return self.code.declared_identifiers.union(self.body_decl.argnames)
@@ -400,7 +446,9 @@ class InheritTag(Tag):
     __keyword__ = 'inherit'
 
     def __init__(self, keyword, attributes, **kwargs):
-        super(InheritTag, self).__init__(keyword, attributes, ('file',), (), ('file',), **kwargs)
+        super(InheritTag, self).__init__(
+                                keyword, attributes, 
+                                ('file',), (), ('file',), **kwargs)
 
 class PageTag(Tag):
     __keyword__ = 'page'
@@ -409,12 +457,16 @@ class PageTag(Tag):
         super(PageTag, self).__init__(
                 keyword, 
                 attributes, 
-                ('cached', 'cache_key', 'cache_timeout', 'cache_type', 'cache_dir', 'cache_url', 'args', 'expression_filter'), 
+                ('cached', 'cache_key', 'cache_timeout', 
+                'cache_type', 'cache_dir', 'cache_url', 
+                'args', 'expression_filter'), 
                 (), 
                 (), 
                 **kwargs)
         self.body_decl = ast.FunctionArgs(attributes.get('args', ''), **self.exception_kwargs)
-        self.filter_args = ast.ArgumentList(attributes.get('expression_filter', ''), **self.exception_kwargs)
+        self.filter_args = ast.ArgumentList(
+                                attributes.get('expression_filter', ''),
+                                **self.exception_kwargs)
 
     def declared_identifiers(self):
         return self.body_decl.argnames
diff --git a/mako/pygen.py b/mako/pygen.py
index 914443bd8be4151fb827748b72bb980f45f19276..aada94d23e3be74ab01a823a6bece33ad1e46ed2 100644
--- a/mako/pygen.py
+++ b/mako/pygen.py
@@ -8,6 +8,7 @@
 
 import re, string
 from StringIO import StringIO
+from mako import exceptions
 
 class PythonPrinter(object):
     def __init__(self, stream):
@@ -84,7 +85,7 @@ class PythonPrinter(object):
                 # probably put extra closures - the resulting
                 # module wont compile.  
                 if len(self.indent_detail) == 0:  
-                    raise "Too many whitespace closures"
+                    raise exceptions.SyntaxException("Too many whitespace closures")
                 self.indent_detail.pop()
         
         if line is None:
@@ -200,7 +201,7 @@ class PythonPrinter(object):
             if self._in_multi_line(entry):
                 self.stream.write(entry + "\n")
             else:
-                entry = string.expandtabs(entry)
+                entry = entry.expandtabs()
                 if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry):
                     stripspace = re.match(r"^([ \t]*)", entry).group(1)
                 self.stream.write(self._indent_line(entry, stripspace) + "\n")
@@ -260,7 +261,7 @@ def adjust_whitespace(text):
         if in_multi_line(line):
             lines.append(line)
         else:
-            line = string.expandtabs(line)
+            line = line.expandtabs()
             if stripspace is None and re.search(r"^[ \t]*[^# \t]", line):
                 stripspace = re.match(r"^([ \t]*)", line).group(1)
             lines.append(_indent_line(line, stripspace))
diff --git a/mako/pyparser.py b/mako/pyparser.py
index 7ae3a4fb6ca17be88a3b0fe28947c4041ec0a346..b90278e2e96dd1eb8beb6ce4b349225d51f07463 100644
--- a/mako/pyparser.py
+++ b/mako/pyparser.py
@@ -12,9 +12,23 @@ module is used.
 
 from StringIO import StringIO
 from mako import exceptions, util
+import operator
+
+if util.py3k:
+    # words that cannot be assigned to (notably 
+    # smaller than the total keys in __builtins__)
+    reserved = set(['True', 'False', 'None', 'print'])
+
+    # the "id" attribute on a function node
+    arg_id = operator.attrgetter('arg')
+else:
+    # words that cannot be assigned to (notably 
+    # smaller than the total keys in __builtins__)
+    reserved = set(['True', 'False', 'None'])
+    
+    # the "id" attribute on a function node
+    arg_id = operator.attrgetter('id')
 
-# words that cannot be assigned to (notably smaller than the total keys in __builtins__)
-reserved = set(['True', 'False', 'None'])
 
 try:
     import _ast
@@ -63,6 +77,18 @@ if _ast:
             for n in node.targets:
                 self.visit(n)
             self.in_assign_targets = in_a
+        
+        if util.py3k:
+            # ExceptHandler is in Python 2, but this
+            # block only works in Python 3 (and is required there)
+            def visit_ExceptHandler(self, node):
+                if node.name is not None:
+                    self._add_declared(node.name)
+                if node.type is not None:
+                    self.listener.undeclared_identifiers.add(node.type.id)
+                for statement in node.body:
+                    self.visit(statement)
+                
         def visit_FunctionDef(self, node):
             self._add_declared(node.name)
             # push function state onto stack.  dont log any
@@ -72,17 +98,19 @@ if _ast:
             saved = {}
             inf = self.in_function
             self.in_function = True
+            
             for arg in node.args.args:
-                if arg.id in self.local_ident_stack:
-                    saved[arg.id] = True
+                if arg_id(arg) in self.local_ident_stack:
+                    saved[arg_id(arg)] = True
                 else:
-                    self.local_ident_stack[arg.id] = True
+                    self.local_ident_stack[arg_id(arg)] = True
             for n in node.body:
                 self.visit(n)
             self.in_function = inf
             for arg in node.args.args:
-                if arg.id not in saved:
-                    del self.local_ident_stack[arg.id]
+                if arg_id(arg) not in saved:
+                    del self.local_ident_stack[arg_id(arg)]
+                    
         def visit_For(self, node):
             # flip around visit
             self.visit(node.iter)
@@ -94,7 +122,9 @@ if _ast:
         def visit_Name(self, node):
             if isinstance(node.ctx, _ast.Store):
                 self._add_declared(node.id)
-            if node.id not in reserved and node.id not in self.listener.declared_identifiers and node.id not in self.local_ident_stack:
+            if node.id not in reserved and \
+                        node.id not in self.listener.declared_identifiers and \
+                        node.id not in self.local_ident_stack:
                 self.listener.undeclared_identifiers.add(node.id)
         def visit_Import(self, node):
             for name in node.names:
@@ -128,9 +158,10 @@ if _ast:
         def __init__(self, listener, **exception_kwargs):
             self.listener = listener
             self.exception_kwargs = exception_kwargs
+            
         def visit_FunctionDef(self, node):
             self.listener.funcname = node.name
-            argnames = [arg.id for arg in node.args.args]
+            argnames = [arg_id(arg) for arg in node.args.args]
             if node.args.vararg:
                 argnames.append(node.args.vararg)
             if node.args.kwarg:
diff --git a/mako/runtime.py b/mako/runtime.py
index 583b79e2416d7047e3db728a3e7cfc840accbe40..95828b48b5de131bfd149636a0ba69e268473e66 100644
--- a/mako/runtime.py
+++ b/mako/runtime.py
@@ -18,6 +18,7 @@ class Context(object):
         self._data.update(data)
         self._kwargs = data.copy()
         self._with_template = None
+        self._outputting_as_unicode = None
         self.namespaces = {}
         
         # "capture" function which proxies to the generic "capture" function
@@ -26,8 +27,13 @@ class Context(object):
         # "caller" stack used by def calls with content
         self.caller_stack = self._data['caller'] = CallerStack()
         
-    lookup = property(lambda self:self._with_template.lookup)
-    kwargs = property(lambda self:self._kwargs.copy())
+    @property
+    def lookup(self):
+        return self._with_template.lookup
+        
+    @property
+    def kwargs(self):
+        return self._kwargs.copy()
     
     def push_caller(self, caller):
         self.caller_stack.append(caller)
@@ -87,6 +93,7 @@ class Context(object):
         c._orig = self._orig
         c._kwargs = self._kwargs
         c._with_template = self._with_template
+        c._outputting_as_unicode = self._outputting_as_unicode
         c.namespaces = self.namespaces
         c.caller_stack = self.caller_stack
         return c
@@ -368,6 +375,7 @@ def _render(template, callable_, args, data, as_unicode=False):
     else:
         buf = util.StringIO()
     context = Context(buf, **data)
+    context._outputting_as_unicode = as_unicode
     context._with_template = template
     _render_context(template, callable_, context, *args, **_kwargs_for_callable(callable_, data))
     return context._pop_buffer().getvalue()
@@ -404,19 +412,24 @@ def _exec_template(callable_, context, args=None, kwargs=None):
         try:
             callable_(context, *args, **kwargs)
         except Exception, e:
-            error = e
+            _render_error(template, context, e)
         except:                
             e = sys.exc_info()[0]
-            error = e
-        if error:
-            if template.error_handler:
-                result = template.error_handler(context, error)
-                if not result:
-                    raise error
-            else:
-                error_template = exceptions.html_error_template()
-                context._buffer_stack[:] = [util.FastEncodingBuffer(error_template.output_encoding, error_template.encoding_errors)]
-                context._with_template = error_template
-                error_template.render_context(context, error=error)
+            _render_error(template, context, e)
     else:
         callable_(context, *args, **kwargs)
+
+
+def _render_error(template, context, error):
+    if template.error_handler:
+        result = template.error_handler(context, error)
+        if not result:
+            raise error
+    else:
+        error_template = exceptions.html_error_template()
+        if context._outputting_as_unicode:
+            context._buffer_stack[:] = [util.FastEncodingBuffer(unicode=True)]
+        else:
+            context._buffer_stack[:] = [util.FastEncodingBuffer(error_template.output_encoding, error_template.encoding_errors)]
+        context._with_template = error_template
+        error_template.render_context(context, error=error)
diff --git a/mako/template.py b/mako/template.py
index 87f68980ca01c3d0a3faa754f3401d707770404e..910468359cb5d8e1e4ba0d2a0d0111bcd97b85ce 100644
--- a/mako/template.py
+++ b/mako/template.py
@@ -66,8 +66,14 @@ class Template(object):
         self.output_encoding = output_encoding
         self.encoding_errors = encoding_errors
         self.disable_unicode = disable_unicode
+
+        if util.py3k and disable_unicode:
+            raise exceptions.UnsupportedError(
+                                    "Mako for Python 3 does not "
+                                    "support disabling Unicode")
+        
         if default_filters is None:
-            if self.disable_unicode:
+            if util.py3k or self.disable_unicode:
                 self.default_filters = ['str']
             else:
                 self.default_filters = ['unicode']
@@ -108,18 +114,18 @@ class Template(object):
                             os.stat(path)[stat.ST_MTIME] < filemtime:
                     _compile_module_file(
                                 self, 
-                                file(filename).read(), 
+                                open(filename, 'rb').read(), 
                                 filename, 
                                 path)
-                module = imp.load_source(self.module_id, path, file(path))
+                module = imp.load_source(self.module_id, path, open(path, 'rb'))
                 del sys.modules[self.module_id]
                 if module._magic_number != codegen.MAGIC_NUMBER:
                     _compile_module_file(
                                 self, 
-                                file(filename).read(), 
+                                open(filename, 'rb').read(), 
                                 filename, 
                                 path)
-                    module = imp.load_source(self.module_id, path, file(path))
+                    module = imp.load_source(self.module_id, path, open(path, 'rb'))
                     del sys.modules[self.module_id]
                 ModuleInfo(module, path, self, filename, None, None)
             else:
@@ -127,7 +133,7 @@ class Template(object):
                 # in memory
                 (code, module) = _compile_text(
                                     self, 
-                                    file(filename).read(), 
+                                    open(filename, 'rb').read(), 
                                     filename)
                 self._source = None
                 self._code = code
@@ -318,7 +324,7 @@ class ModuleInfo(object):
         if self.module_source is not None:
             return self.module_source
         else:
-            return file(self.module_filename).read()
+            return open(self.module_filename).read()
     
     @property
     def source(self):
@@ -331,10 +337,10 @@ class ModuleInfo(object):
                 return self.template_source
         else:
             if self.module._source_encoding:
-                return file(self.template_filename).read().\
+                return open(self.template_filename, 'rb').read().\
                                 decode(self.module._source_encoding)
             else:
-                return file(self.template_filename).read()
+                return open(self.template_filename).read()
         
 def _compile_text(template, text, filename):
     identifier = template.module_id
@@ -355,7 +361,7 @@ def _compile_text(template, text, filename):
                             generate_magic_comment=template.disable_unicode)
 
     cid = identifier
-    if isinstance(cid, unicode):
+    if not util.py3k and isinstance(cid, unicode):
         cid = cid.encode()
     module = types.ModuleType(cid)
     code = compile(source, cid, 'exec')
diff --git a/mako/util.py b/mako/util.py
index 88076c313c72cf67aec8cfa099817c5ff7de5bc6..dcac5d757e3d98a0a9ba4ff6318f0e2762d29dad 100644
--- a/mako/util.py
+++ b/mako/util.py
@@ -6,16 +6,20 @@
 
 import sys
 
-try:
-    from cStringIO import StringIO
-except:
-    from StringIO import StringIO
 
 py3k = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0)
 jython = sys.platform.startswith('java')
 win32 = sys.platform.startswith('win')
 
-import codecs, re, weakref, os, time
+if py3k:
+    from io import StringIO
+else:
+    try:
+        from cStringIO import StringIO
+    except:
+        from StringIO import StringIO
+
+import codecs, re, weakref, os, time, operator
 
 try:
     import threading
@@ -60,6 +64,10 @@ def to_list(x, default=None):
     else:
         return x
 
+
+    
+
+
 class SetLikeDict(dict):
     """a dictionary that has some setlike methods on it"""
     def union(self, other):
@@ -84,6 +92,9 @@ class FastEncodingBuffer(object):
         self.unicode = unicode
         self.errors = errors
         self.write = self.data.append
+    
+    def truncate(self):
+        self.data =[]
         
     def getvalue(self):
         if self.encoding:
@@ -138,8 +149,8 @@ class LRUCache(dict):
     
     def _manage_size(self):
         while len(self) > self.capacity + self.capacity * self.threshold:
-            bytime = dict.values(self)
-            bytime.sort(lambda a, b: cmp(b.timestamp, a.timestamp))
+            bytime = sorted(dict.values(self), 
+                            key=operator.attrgetter('timestamp'), reverse=True)
             for item in bytime[self.capacity:]:
                 try:
                     del self[item.key]
@@ -154,13 +165,13 @@ _PYTHON_MAGIC_COMMENT_re = re.compile(
     re.VERBOSE)
 
 def parse_encoding(fp):
-    """Deduce the encoding of a source file from magic comment.
+    """Deduce the encoding of a Python source file (binary mode) from magic comment.
 
     It does this in the same way as the `Python interpreter`__
 
     .. __: http://docs.python.org/ref/encodings.html
 
-    The ``fp`` argument should be a seekable file object.
+    The ``fp`` argument should be a seekable file object in binary mode.
     """
     pos = fp.tell()
     fp.seek(0)
@@ -170,11 +181,11 @@ def parse_encoding(fp):
         if has_bom:
             line1 = line1[len(codecs.BOM_UTF8):]
 
-        m = _PYTHON_MAGIC_COMMENT_re.match(line1)
+        m = _PYTHON_MAGIC_COMMENT_re.match(line1.decode('ascii', 'ignore'))
         if not m:
             try:
                 import parser
-                parser.suite(line1)
+                parser.suite(line1.decode('ascii', 'ignore'))
             except (ImportError, SyntaxError):
                 # Either it's a real syntax error, in which case the source
                 # is not valid python source, or line2 is a continuation of
@@ -183,7 +194,7 @@ def parse_encoding(fp):
                 pass
             else:
                 line2 = fp.readline()
-                m = _PYTHON_MAGIC_COMMENT_re.match(line2)
+                m = _PYTHON_MAGIC_COMMENT_re.match(line2.decode('ascii', 'ignore'))
 
         if has_bom:
             if m:
diff --git a/setup.py b/setup.py
index caed701c6ade47cd6db1d3f3576f0b67698bd8be..dda072f4eca6910fe693f5b028988252a8f47146 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,13 @@
 from setuptools import setup, find_packages
 import os
 import re
+import sys
+
+extra = {}
+if sys.version_info >= (3, 0):
+    extra.update(
+        use_2to3=True,
+    )
 
 v = file(os.path.join(os.path.dirname(__file__), 'mako', '__init__.py'))
 VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1)
@@ -25,10 +32,11 @@ SVN version:
 
 """,
       classifiers=[
-      "Development Status :: 5 - Production/Stable",
+      'Development Status :: 5 - Production/Stable',
       'Environment :: Web Environment',
       'Intended Audience :: Developers',
       'Programming Language :: Python',
+      'Programming Language :: Python :: 3',
       'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
       ],
       keywords='wsgi myghty mako',
diff --git a/test/__init__.py b/test/__init__.py
index c8c2a4dd208b6393e184c6a5eb038e175e83306f..1bad221824d004aca47276a130cd427515bd7e22 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -15,17 +15,26 @@ class TemplateTest(unittest.TestCase):
         return Template(uri=filename, filename=filepath, module_directory=module_base, **kw)
     
     def _file_path(self, filename):
+        name, ext = os.path.splitext(filename)
+        
+        if py3k:
+            py3k_path = os.path.join(template_base, name + "_py3k" + ext)
+            if os.path.exists(py3k_path):
+                return py3k_path
+        
         return os.path.join(template_base, filename)
         
     def _do_file_test(self, filename, expected, filters=None, 
                         unicode_=True, template_args=None, **kw):
         t1 = self._file_template(filename, **kw)
-        self._do_test(t1, expected, filters=filters, unicode_=unicode_, template_args=template_args)
+        self._do_test(t1, expected, filters=filters, 
+                        unicode_=unicode_, template_args=template_args)
     
     def _do_memory_test(self, source, expected, filters=None, 
                         unicode_=True, template_args=None, **kw):
         t1 = Template(text=source, **kw)
-        self._do_test(t1, expected, filters=filters, unicode_=unicode_, template_args=template_args)
+        self._do_test(t1, expected, filters=filters, 
+                        unicode_=unicode_, template_args=template_args)
     
     def _do_test(self, template, expected, filters=None, template_args=None, unicode_=True):
         if template_args is None:
diff --git a/test/templates/chs_unicode_py3k.html b/test/templates/chs_unicode_py3k.html
new file mode 100644
index 0000000000000000000000000000000000000000..35b888ddcb7a3a8e11b2bcc5c063f250b85d6985
--- /dev/null
+++ b/test/templates/chs_unicode_py3k.html
@@ -0,0 +1,11 @@
+## -*- encoding:utf8 -*-
+<%
+ msg = '新中国的主席'
+%>
+
+<%def name="welcome(who, place='北京')">
+Welcome ${who} to ${place}.
+</%def>
+
+${name} 是 ${msg}<br/>
+${welcome('ä½ ')}
diff --git a/test/templates/read_unicode_py3k.html b/test/templates/read_unicode_py3k.html
new file mode 100644
index 0000000000000000000000000000000000000000..380d35622ff51dc01d7a760f686723d8ed741ad8
--- /dev/null
+++ b/test/templates/read_unicode_py3k.html
@@ -0,0 +1,10 @@
+<% 
+try:
+    file_content = open(path)
+except:
+    raise "Should never execute here"
+doc_content = ''.join(file_content.readlines())
+file_content.close()
+%>
+
+${bytes(doc_content, encoding='utf-8')}
diff --git a/test/templates/unicode_arguments_py3k.html b/test/templates/unicode_arguments_py3k.html
new file mode 100644
index 0000000000000000000000000000000000000000..97e6d3a8afbc59545933598779c86552a23dffc7
--- /dev/null
+++ b/test/templates/unicode_arguments_py3k.html
@@ -0,0 +1,10 @@
+# coding: utf-8
+
+<%def name="my_def(x)">
+    x is: ${x}
+</%def>
+
+${my_def('drôle de petit voix m’a réveillé')}
+<%self:my_def x='drôle de petit voix m’a réveillé'/>
+<%self:my_def x="${'drôle de petit voix m’a réveillé'}"/>
+<%call expr="my_def('drôle de petit voix m’a réveillé')"/>
diff --git a/test/templates/unicode_code_py3k.html b/test/templates/unicode_code_py3k.html
new file mode 100644
index 0000000000000000000000000000000000000000..76ed9cc3a7c197ab62a8a6acd0b204689eb4a423
--- /dev/null
+++ b/test/templates/unicode_code_py3k.html
@@ -0,0 +1,7 @@
+## -*- coding: utf-8 -*-
+<%
+    x = "drôle de petit voix m’a réveillé."
+%>
+% if x=="drôle de petit voix m’a réveillé.":
+    hi, ${x}
+% endif
diff --git a/test/templates/unicode_expr_py3k.html b/test/templates/unicode_expr_py3k.html
new file mode 100644
index 0000000000000000000000000000000000000000..48982573c14d871b228cbadbdb14ca3851c9689c
--- /dev/null
+++ b/test/templates/unicode_expr_py3k.html
@@ -0,0 +1,2 @@
+## -*- coding: utf-8 -*-
+${"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"}
diff --git a/test/test_ast.py b/test/test_ast.py
index ed2145619a372db9c172a88a3285e9bcdbbfbffd..bfdfd90e2b5250a23ad7be57394c1dd4a1400e3a 100644
--- a/test/test_ast.py
+++ b/test/test_ast.py
@@ -1,14 +1,12 @@
 import unittest
 
 from mako import ast, exceptions, pyparser, util
+from test import eq_
 
 exception_kwargs = {'source':'', 'lineno':0, 'pos':0, 'filename':''}
 
 class AstParseTest(unittest.TestCase):
-    def setUp(self):
-        pass
-    def tearDown(self):
-        pass
+
     def test_locate_identifiers(self):
         """test the location of identifiers in a python code string"""
         code = """
@@ -21,8 +19,8 @@ foo.hoho.lala.bar = 7 + gah.blah + u + blah
 for lar in (1,2,3):
     gh = 5
     x = 12
-print "hello world, ", a, b
-print "Another expr", c
+("hello world, ", a, b)
+("Another expr", c)
 """
         parsed = ast.PythonCode(code, **exception_kwargs)
         assert parsed.declared_identifiers == set(['a','b','c', 'g', 'h', 'i', 'u', 'k', 'j', 'gh', 'lar', 'x'])
@@ -51,7 +49,7 @@ for x in data:
         code = """
 x = x + 5
 for y in range(1, y):
-    print "hi"
+    ("hi",)
 [z for z in range(1, z)]
 (q for q in range (1, q))
 """
@@ -59,9 +57,17 @@ for y in range(1, y):
         assert parsed.undeclared_identifiers == set(['x', 'y', 'z', 'q', 'range'])
     
     def test_locate_identifiers_4(self):
-        code = """
+        if util.py3k:
+            code = """
+x = 5
+(y, )
+def mydef(mydefarg):
+    print("mda is", mydefarg)
+"""    
+        else:
+            code = """
 x = 5
-print y
+(y, )
 def mydef(mydefarg):
     print "mda is", mydefarg
 """    
@@ -70,7 +76,16 @@ def mydef(mydefarg):
         assert parsed.declared_identifiers == set(['mydef', 'x'])
     
     def test_locate_identifiers_5(self):
-        code = """
+        if util.py3k:
+            code = """
+try:
+    print(x)
+except:
+    print(y)
+"""
+        else:
+            
+            code = """
 try:
     print x
 except:
@@ -86,8 +101,15 @@ def foo():
 """
         parsed = ast.PythonCode(code, **exception_kwargs)
         assert parsed.undeclared_identifiers == set(['bar'])
-    
-        code = """
+        
+        if util.py3k:
+            code = """
+def lala(x, y):
+    return x, y, z
+print(x)
+"""
+        else:
+            code = """
 def lala(x, y):
     return x, y, z
 print x
@@ -95,8 +117,17 @@ print x
         parsed = ast.PythonCode(code, **exception_kwargs)
         assert parsed.undeclared_identifiers == set(['z', 'x'])
         assert parsed.declared_identifiers == set(['lala'])
-    
-        code = """
+        
+        if util.py3k:
+            code = """
+def lala(x, y):
+    def hoho():
+        def bar():
+            z = 7
+print(z)
+"""
+        else:
+            code = """
 def lala(x, y):
     def hoho():
         def bar():
@@ -131,11 +162,7 @@ class Hi(object):
 from foo import *
 import x as bar
 """
-        try:
-            parsed = ast.PythonCode(code, **exception_kwargs)
-            assert False
-        except exceptions.CompileException, e:
-            assert str(e).startswith("'import *' is not supported")
+        self.assertRaises(exceptions.CompileException, ast.PythonCode, code, **exception_kwargs)
             
     def test_python_fragment(self):
         parsed = ast.PythonFragment("for x in foo:", **exception_kwargs)
@@ -144,9 +171,12 @@ import x as bar
         
         parsed = ast.PythonFragment("try:", **exception_kwargs)
 
-        parsed = ast.PythonFragment("except MyException, e:", **exception_kwargs)
-        assert parsed.declared_identifiers == set(['e'])
-        assert parsed.undeclared_identifiers == set(['MyException'])
+        if util.py3k:
+            parsed = ast.PythonFragment("except MyException as e:", **exception_kwargs)
+        else:
+            parsed = ast.PythonFragment("except MyException, e:", **exception_kwargs)
+        eq_(parsed.declared_identifiers, set(['e']))
+        eq_(parsed.undeclared_identifiers, set(['MyException']))
     
     def test_argument_list(self):
         parsed = ast.ArgumentList("3, 5, 'hi', x+5, context.get('lala')", **exception_kwargs)
@@ -184,8 +214,6 @@ import x as bar
         code = "str((x+7*y) / foo.bar(5,6)) + lala('ho')"
         astnode = pyparser.parse(code)
         newcode = pyparser.ExpressionGenerator(astnode).value()
-        #print "newcode:" + newcode
-        #print "result:" + eval(code, local_dict)
         assert (eval(code, local_dict) == eval(newcode, local_dict))
         
         a = ["one", "two", "three"]
@@ -195,8 +223,6 @@ import x as bar
         code = "a[2] + hoho['somevalue'] + repr(g[3:5]) + repr(g[3:]) + repr(g[:5])"
         astnode = pyparser.parse(code)
         newcode = pyparser.ExpressionGenerator(astnode).value()
-        #print newcode
-        #print "result:", eval(code, local_dict)
         assert(eval(code, local_dict) == eval(newcode, local_dict))
         
         local_dict={'f':lambda :9, 'x':7}
@@ -209,7 +235,6 @@ import x as bar
             local_dict={}
             astnode = pyparser.parse(code)
             newcode = pyparser.ExpressionGenerator(astnode).value()
-            #print code, newcode
             assert(eval(code, local_dict)) == eval(newcode, local_dict), "%s != %s" % (code, newcode)
 
     
diff --git a/test/test_def.py b/test/test_def.py
index 6cb1fdfd36a8b807739255e0033bacc247b3d37e..1f7a39aa8ee55f89fbb692d51788a1b82a72021f 100644
--- a/test/test_def.py
+++ b/test/test_def.py
@@ -1,9 +1,9 @@
 from mako.template import Template
 from mako import lookup
-import unittest
+from test import TemplateTest
 from util import flatten_result, result_lines
 
-class DefTest(unittest.TestCase):
+class DefTest(TemplateTest):
     def test_def_noargs(self):
         template = Template("""
         
@@ -61,6 +61,7 @@ class DefTest(unittest.TestCase):
 
     def test_toplevel(self):
         """test calling a def from the top level"""
+
         template = Template("""
         
             this is the body
@@ -73,15 +74,20 @@ class DefTest(unittest.TestCase):
                 this is b, ${x} ${y}
             </%def>
                 
-        """, output_encoding='utf-8')
-        assert flatten_result(template.get_def("a").render()) == "this is a"
-        assert flatten_result(template.get_def("b").render(x=10, y=15)) == "this is b, 10 15"
-        assert flatten_result(template.get_def("body").render()) == "this is the body"
+        """)
+        
+        self._do_test(template.get_def("a"), "this is a", filters=flatten_result)
+        self._do_test(template.get_def("b"), "this is b, 10 15", 
+                                                            template_args={'x':10, 'y':15}, 
+                                                            filters=flatten_result)
+        self._do_test(template.get_def("body"), "this is the body", filters=flatten_result)
+        
         
-class ScopeTest(unittest.TestCase):
+class ScopeTest(TemplateTest):
     """test scoping rules.  The key is, enclosing scope always takes precedence over contextual scope."""
+    
     def test_scope_one(self):
-        t = Template("""
+        self._do_memory_test("""
         <%def name="a()">
             this is a, and y is ${y}
         </%def>
@@ -94,8 +100,11 @@ class ScopeTest(unittest.TestCase):
 
         ${a()}
 
-""")
-        assert flatten_result(t.render(y=None)) == "this is a, and y is None this is a, and y is 7"
+""",
+            "this is a, and y is None this is a, and y is 7",
+            filters=flatten_result,
+            template_args={'y':None}
+        )
 
     def test_scope_two(self):
         t = Template("""
@@ -372,7 +381,7 @@ class ScopeTest(unittest.TestCase):
             "this is a, x is 15"
         ]
 
-class NestedDefTest(unittest.TestCase):
+class NestedDefTest(TemplateTest):
     def test_nested_def(self):
         t = Template("""
 
@@ -512,7 +521,7 @@ class NestedDefTest(unittest.TestCase):
 """)
         assert flatten_result(t.render(x=5)) == "b. c. x is 10. a: x is 5 x is 5"
             
-class ExceptionTest(unittest.TestCase):
+class ExceptionTest(TemplateTest):
     def test_raise(self):
         template = Template("""
             <%
diff --git a/test/test_exceptions.py b/test/test_exceptions.py
index 52c9544176ecc0629a747ef1b0c74a8af3368472..57b3bacbb6577991ff2bfc88d464da215603cda4 100644
--- a/test/test_exceptions.py
+++ b/test/test_exceptions.py
@@ -2,7 +2,7 @@
 import sys
 import unittest
 
-from mako import exceptions
+from mako import exceptions, util
 from mako.template import Template
 from mako.lookup import TemplateLookup
 from util import result_lines
@@ -15,9 +15,9 @@ class ExceptionsTest(unittest.TestCase):
 """
         try:
             template = Template(code)
-            template.render()
+            template.render_unicode()
         except exceptions.CompileException, ce:
-            html_error = exceptions.html_error_template().render()
+            html_error = exceptions.html_error_template().render_unicode()
             assert ("CompileException: Fragment 'i = 0' is not a partial "
                     "control statement") in html_error
             assert '<style>' in html_error
@@ -26,13 +26,13 @@ class ExceptionsTest(unittest.TestCase):
             assert html_error_stripped.startswith('<html>')
             assert html_error_stripped.endswith('</html>')
 
-            not_full = exceptions.html_error_template().render(full=False)
+            not_full = exceptions.html_error_template().render_unicode(full=False)
             assert '<html>' not in not_full
             assert '</html>' not in not_full
             assert '<style>' in not_full
             assert '</style>' in not_full
 
-            no_css = exceptions.html_error_template().render(css=False)
+            no_css = exceptions.html_error_template().render_unicode(css=False)
             assert '<style>' not in no_css
             assert '</style>' not in no_css
         else:
@@ -41,20 +41,33 @@ class ExceptionsTest(unittest.TestCase):
 
     def test_utf8_html_error_template(self):
         """test the html_error_template with a Template containing utf8 chars"""
-        code = """# -*- coding: utf-8 -*-
+        
+        if util.py3k:
+            code = """# -*- coding: utf-8 -*-
+% if 2 == 2: /an error
+${'привет'}
+% endif
+"""
+        else:
+            code = """# -*- coding: utf-8 -*-
 % if 2 == 2: /an error
 ${u'привет'}
 % endif
 """
         try:
             template = Template(code)
-            template.render()
+            template.render_unicode()
         except exceptions.CompileException, ce:
             html_error = exceptions.html_error_template().render()
             assert ("CompileException: Fragment 'if 2 == 2: /an "
                     "error' is not a partial control "
-                    "statement at line: 2 char: 1") in html_error
-            assert u"3 ${u'привет'}".encode(sys.getdefaultencoding(),
+                    "statement at line: 2 char: 1") in html_error.decode('utf-8')
+                    
+            if util.py3k:
+                assert u"3 ${'привет'}".encode(sys.getdefaultencoding(),
+                                            'htmlentityreplace') in html_error
+            else:
+                assert u"3 ${u'привет'}".encode(sys.getdefaultencoding(),
                                             'htmlentityreplace') in html_error
         else:
             assert False, ("This function should trigger a CompileException, "
@@ -66,8 +79,12 @@ ${u'привет'}
             raise RuntimeError('test')
         except:
             html_error = exceptions.html_error_template().render()
-            assert 'RuntimeError: test' in html_error
-            assert "foo = u'&#x65E5;&#x672C;'" in html_error
+            if util.py3k:
+                assert 'RuntimeError: test' in html_error.decode('utf-8')
+                assert u"foo = '日本'" in html_error.decode('utf-8')
+            else:
+                assert 'RuntimeError: test' in html_error
+                assert "foo = u'&#x65E5;&#x672C;'" in html_error
 
 
     def test_py_unicode_error_html_error_template(self):
@@ -75,8 +92,7 @@ ${u'привет'}
             raise RuntimeError(u'日本')
         except:
             html_error = exceptions.html_error_template().render()
-            assert 'RuntimeError: &#x65E5;&#x672C;' in html_error
-            assert "RuntimeError(u'&#x65E5;&#x672C;')" in html_error
+            assert u"RuntimeError: 日本".encode('ascii', 'ignore') in html_error
 
     def test_format_exceptions(self):
         l = TemplateLookup(format_exceptions=True)
@@ -90,16 +106,21 @@ ${foobar}
         ${self.body()}
         """)
 
-        assert '<div class="sourceline">${foobar}</div>' in result_lines(l.get_template("foo.html").render())
+        assert '<div class="sourceline">${foobar}</div>' in result_lines(l.get_template("foo.html").render_unicode())
     
     def test_utf8_format_exceptions(self):
         """test that htmlentityreplace formatting is applied to exceptions reported with format_exceptions=True"""
         
         l = TemplateLookup(format_exceptions=True)
+        if util.py3k:
+            l.put_string("foo.html", """# -*- coding: utf-8 -*-\n${'привет' + foobar}""")
+        else:
+            l.put_string("foo.html", """# -*- coding: utf-8 -*-\n${u'привет' + foobar}""")
 
-        l.put_string("foo.html", """# -*- coding: utf-8 -*-
-${u'привет' + foobar}
-""")
-
-        assert '''<div class="highlight">2 ${u\'&#x43F;&#x440;&#x438;&#x432;&#x435;&#x442;\' + foobar}</div>''' in result_lines(l.get_template("foo.html").render())
+        if util.py3k:
+            assert u'<div class="sourceline">${\'привет\' + foobar}</div>'\
+                in result_lines(l.get_template("foo.html").render().decode('utf-8'))
+        else:
+            assert '<div class="highlight">2 ${u\'&#x43F;&#x440;&#x438;&#x432;&#x435;&#x442;\' + foobar}</div>' \
+                in result_lines(l.get_template("foo.html").render().decode('utf-8'))
         
diff --git a/test/test_inheritance.py b/test/test_inheritance.py
index c9c6990e1b4cebcb880a05afa300beeb39a99521..9f978d2b2e67ed23ffc85b927121e2b3916cacd4 100644
--- a/test/test_inheritance.py
+++ b/test/test_inheritance.py
@@ -1,5 +1,5 @@
 from mako.template import Template
-from mako import lookup
+from mako import lookup, util
 import unittest
 from util import flatten_result, result_lines
 
@@ -187,10 +187,10 @@ ${next.body()}
             this is the base.
 
             <%
-            sorted = pageargs.items()
-            sorted.sort()
+            sorted_ = pageargs.items()
+            sorted_ = sorted(sorted_)
             %>
-            pageargs: (type: ${type(pageargs)}) ${sorted}
+            pageargs: (type: ${type(pageargs)}) ${sorted_}
             <%def name="foo()">
                 ${next.body(**context.kwargs)}
             </%def>
@@ -202,11 +202,20 @@ ${next.body()}
             <%page args="x, y, z=7"/>
             print ${x}, ${y}, ${z}
         """)
-        assert result_lines(collection.get_template('index').render(x=5,y=10)) == [
-            "this is the base.",
-            "pageargs: (type: <type 'dict'>) [('x', 5), ('y', 10)]",
-            "print 5, 10, 7"
-        ]
+        
+        if util.py3k:
+            assert result_lines(collection.get_template('index').render_unicode(x=5,y=10)) == [
+                "this is the base.",
+                "pageargs: (type: <class 'dict'>) [('x', 5), ('y', 10)]",
+                "print 5, 10, 7"
+            ]
+        else:
+            assert result_lines(collection.get_template('index').render_unicode(x=5,y=10)) == [
+                "this is the base.",
+                "pageargs: (type: <type 'dict'>) [('x', 5), ('y', 10)]",
+                "print 5, 10, 7"
+            ]
+        
     def test_pageargs_2(self):
         collection = lookup.TemplateLookup()
         collection.put_string("base", """
diff --git a/test/test_lexer.py b/test/test_lexer.py
index d93486093cc0f774d28579ac05ab5791e99b0d49..00d16afdf2462af156457c250ff6ab0f36edb297 100644
--- a/test/test_lexer.py
+++ b/test/test_lexer.py
@@ -1,14 +1,45 @@
 import unittest
 
 from mako.lexer import Lexer
-from mako import exceptions
+from mako import exceptions, util
 from util import flatten_result, result_lines
 from mako.template import Template
 import re
-from test import TemplateTest, template_base, skip_if
-
-
+from test import TemplateTest, template_base, skip_if, eq_
+
+# create fake parsetree classes which are constructed
+# exactly as the repr() of a real parsetree object.
+# this allows us to use a Python construct as the source
+# of a comparable repr(), which is also hit by the 2to3 tool.
+
+def repr_arg(x):
+    if isinstance(x, dict):
+        return util.sorted_dict_repr(x)
+    else:
+        return repr(x)
+
+from mako import parsetree
+for cls in parsetree.__dict__.values():
+    if isinstance(cls, type) and \
+        issubclass(cls, parsetree.Node):
+        clsname = cls.__name__
+        exec ("""
+class %s(object):
+    def __init__(self, *args):
+        self.args = args
+    def __repr__(self):
+        return "%%s(%%s)" %% (
+            self.__class__.__name__,
+            ", ".join(repr_arg(x) for x in self.args)
+            )
+""" % clsname) in locals()
+    
+    
 class LexerTest(TemplateTest):
+    
+    def _compare(self, node, expected):
+        eq_(repr(node), repr(expected))
+    
     def test_text_and_tag(self):
         template = """
 <b>Hello world</b>
@@ -19,7 +50,10 @@ class LexerTest(TemplateTest):
         and some more text.
 """
         node = Lexer(template).parse()
-        assert repr(node) == r"""TemplateNode({}, [Text(u'\n<b>Hello world</b>\n        ', (1, 1)), DefTag(u'def', {u'name': u'foo()'}, (3, 9), ["Text(u'\\n                this is a def.\\n        ', (3, 28))"]), Text(u'\n        \n        and some more text.\n', (5, 16))])"""
+        self._compare(
+            node,
+            TemplateNode({}, [Text(u'\n<b>Hello world</b>\n        ', (1, 1)), DefTag(u'def', {u'name': u'foo()'}, (3, 9), [Text(u'\n                this is a def.\n        ', (3, 28))]), Text(u'\n        \n        and some more text.\n', (5, 16))])
+        )
 
     def test_unclosed_tag(self):
         template = """
@@ -96,7 +130,10 @@ class LexerTest(TemplateTest):
         % endif
         """
         node = Lexer(template).parse()
-        assert repr(node) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), Comment(u'comment', (2, 1)), ControlLine(u'if', u'if foo:', False, (3, 1)), Text(u'            hi\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'        ', (6, 1)), TextTag(u'text', {}, (6, 9), ['Text(u\'\\n            # more code\\n            \\n            % more code\\n            <%illegal compionent>/></>\\n            <%def name="laal()">def</%def>\\n            \\n            \\n        \', (6, 16))']), Text(u'\n\n        ', (14, 17)), DefTag(u'def', {u'name': u'foo()'}, (16, 9), ["Text(u'this is foo', (16, 28))"]), Text(u'\n        \n', (16, 46)), ControlLine(u'if', u'if bar:', False, (18, 1)), Text(u'            code\n', (19, 1)), ControlLine(u'if', u'endif', True, (20, 1)), Text(u'        ', (21, 1))])"""
+        self._compare(
+            node,
+            TemplateNode({}, [Text(u'\n', (1, 1)), Comment(u'comment', (2, 1)), ControlLine(u'if', u'if foo:', False, (3, 1)), Text(u'            hi\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'        ', (6, 1)), TextTag(u'text', {}, (6, 9), [Text(u'\n            # more code\n            \n            % more code\n            <%illegal compionent>/></>\n            <%def name="laal()">def</%def>\n            \n            \n        ', (6, 16))]), Text(u'\n\n        ', (14, 17)), DefTag(u'def', {u'name': u'foo()'}, (16, 9), [Text(u'this is foo', (16, 28))]), Text(u'\n        \n', (16, 46)), ControlLine(u'if', u'if bar:', False, (18, 1)), Text(u'            code\n', (19, 1)), ControlLine(u'if', u'endif', True, (20, 1)), Text(u'        ', (21, 1))])
+        )
         
     def test_def_syntax(self):
         template = """
@@ -122,7 +159,10 @@ class LexerTest(TemplateTest):
         """    
 
         node = Lexer(template).parse()
-        assert repr(node) == r"""TemplateNode({}, [Text(u'\n            ', (1, 1)), DefTag(u'def', {u'name': u'adef()'}, (2, 13), ["Text(u'\\n              adef\\n            ', (2, 36))"]), Text(u'\n        ', (4, 20))])"""
+        self._compare(
+            node, 
+            TemplateNode({}, [Text(u'\n            ', (1, 1)), DefTag(u'def', {u'name': u'adef()'}, (2, 13), [Text(u'\n              adef\n            ', (2, 36))]), Text(u'\n        ', (4, 20))])
+        )
 
     def test_ns_tag_closed(self):
         template = """
@@ -130,14 +170,20 @@ class LexerTest(TemplateTest):
             <%self:go x="1" y="2" z="${'hi' + ' ' + 'there'}"/>
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'2', u'z': u"${'hi' + ' ' + 'there'}"}, (3, 13), []), Text(u'\n        ', (3, 64))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'2', u'z': u"${'hi' + ' ' + 'there'}"}, (3, 13), []), Text(u'\n        ', (3, 64))])
+        )
     
     def test_ns_tag_empty(self):
         template = """
             <%form:option value=""></%form:option>
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n            ', (1, 1)), CallNamespaceTag(u'form:option', {u'value': u''}, (2, 13), []), Text(u'\n        ', (2, 51))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n            ', (1, 1)), CallNamespaceTag(u'form:option', {u'value': u''}, (2, 13), []), Text(u'\n        ', (2, 51))])
+        )
         
     def test_ns_tag_open(self):
         template = """
@@ -147,19 +193,26 @@ class LexerTest(TemplateTest):
             </%self:go>
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'${process()}'}, (3, 13), ["Text(u'\\n                this is the body\\n            ', (3, 46))"]), Text(u'\n        ', (5, 24))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), CallNamespaceTag(u'self:go', {u'x': u'1', u'y': u'${process()}'}, (3, 13), [Text(u'\n                this is the body\n            ', (3, 46))]), Text(u'\n        ', (5, 24))])
+        )
         
     def test_expr_in_attribute(self):
         """test some slightly trickier expressions.
         
-        you can still trip up the expression parsing, though, unless we integrated really deeply somehow with AST."""
+        you can still trip up the expression parsing, 
+        though, unless we integrated really deeply somehow with AST."""
+        
         template = """
             <%call expr="foo>bar and 'lala' or 'hoho'"/>
             <%call expr='foo<bar and hoho>lala and "x" + "y"'/>
         """
         nodes = Lexer(template).parse()
-        #print nodes
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n            ', (1, 1)), CallTag(u'call', {u'expr': u"foo>bar and 'lala' or 'hoho'"}, (2, 13), []), Text(u'\n            ', (2, 57)), CallTag(u'call', {u'expr': u'foo<bar and hoho>lala and "x" + "y"'}, (3, 13), []), Text(u'\n        ', (3, 64))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n            ', (1, 1)), CallTag(u'call', {u'expr': u"foo>bar and 'lala' or 'hoho'"}, (2, 13), []), Text(u'\n            ', (2, 57)), CallTag(u'call', {u'expr': u'foo<bar and hoho>lala and "x" + "y"'}, (3, 13), []), Text(u'\n        ', (3, 64))])
+        )
     
         
     def test_pagetag(self):
@@ -169,7 +222,10 @@ class LexerTest(TemplateTest):
             some template
         """    
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n            ', (1, 1)), PageTag(u'page', {u'args': u'a, b', u'cached': u'True'}, (2, 13), []), Text(u'\n            \n            some template\n        ', (2, 48))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n            ', (1, 1)), PageTag(u'page', {u'args': u'a, b', u'cached': u'True'}, (2, 13), []), Text(u'\n            \n            some template\n        ', (2, 48))])
+        )
         
     def test_nesting(self):
         template = """
@@ -182,10 +238,38 @@ class LexerTest(TemplateTest):
         
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        \n        ', (1, 1)), NamespaceTag(u'namespace', {u'name': u'ns'}, (3, 9), ["Text(u'\\n            ', (3, 31))", 'DefTag(u\'def\', {u\'name\': u\'lala(hi, there)\'}, (4, 13), ["Text(u\'\\\\n                \', (4, 42))", "CallTag(u\'call\', {u\'expr\': u\'something()\'}, (5, 17), [])", "Text(u\'\\\\n            \', (5, 44))"])', "Text(u'\\n        ', (6, 20))"]), Text(u'\n        \n        ', (7, 22))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        \n        ', (1, 1)), NamespaceTag(u'namespace', {u'name': u'ns'}, (3, 9), [Text(u'\n            ', (3, 31)), DefTag(u'def', {u'name': u'lala(hi, there)'}, (4, 13), [Text(u'\n                ', (4, 42)), CallTag(u'call', {u'expr': u'something()'}, (5, 17), []), Text(u'\n            ', (5, 44))]), Text(u'\n        ', (6, 20))]), Text(u'\n        \n        ', (7, 22))])
+        )
     
-    def test_code(self):
-        template = """
+    if util.py3k:
+        def test_code(self):
+            template = \
+                    """
+        some text
+        
+        <%
+            print("hi")
+            for x in range(1,5):
+                print(x)
+        %>
+        
+        more text
+        
+        <%!
+            import foo
+        %>
+        """
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Text(u'\n        some text\n        \n        ', (1, 1)), Code(u'\nprint("hi")\nfor x in range(1,5):\n    print(x)\n        \n', False, (4, 9)), Text(u'\n        \n        more text\n        \n        ', (8, 11)), Code(u'\nimport foo\n        \n', True, (12, 9)), Text(u'\n        ', (14, 11))])
+            )
+    else:
+        def test_code(self):
+            template = \
+                    """
         some text
         
         <%
@@ -200,9 +284,11 @@ class LexerTest(TemplateTest):
             import foo
         %>
         """
-        nodes = Lexer(template).parse()
-        #print nodes
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        some text\n        \n        ', (1, 1)), Code(u'\nprint "hi"\nfor x in range(1,5):\n    print x\n        \n', False, (4, 9)), Text(u'\n        \n        more text\n        \n        ', (8, 11)), Code(u'\nimport foo\n        \n', True, (12, 9)), Text(u'\n        ', (14, 11))])"""
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Text(u'\n        some text\n        \n        ', (1, 1)), Code(u'\nprint "hi"\nfor x in range(1,5):\n    print x\n        \n', False, (4, 9)), Text(u'\n        \n        more text\n        \n        ', (8, 11)), Code(u'\nimport foo\n        \n', True, (12, 9)), Text(u'\n        ', (14, 11))])
+            )
     
     def test_code_and_tags(self):
         template = """
@@ -225,7 +311,10 @@ class LexerTest(TemplateTest):
     result: <%call expr="foo.x(result)"/>
 """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), NamespaceTag(u'namespace', {u'name': u'foo'}, (2, 1), ["Text(u'\\n    ', (2, 24))", 'DefTag(u\'def\', {u\'name\': u\'x()\'}, (3, 5), ["Text(u\'\\\\n        this is x\\\\n    \', (3, 22))"])', "Text(u'\\n    ', (5, 12))", 'DefTag(u\'def\', {u\'name\': u\'y()\'}, (6, 5), ["Text(u\'\\\\n        this is y\\\\n    \', (6, 22))"])', "Text(u'\\n', (8, 12))"]), Text(u'\n\n', (9, 14)), Code(u'\nresult = []\ndata = get_data()\nfor x in data:\n    result.append(x+7)\n\n', False, (11, 1)), Text(u'\n\n    result: ', (16, 3)), CallTag(u'call', {u'expr': u'foo.x(result)'}, (18, 13), []), Text(u'\n', (18, 42))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n', (1, 1)), NamespaceTag(u'namespace', {u'name': u'foo'}, (2, 1), [Text(u'\n    ', (2, 24)), DefTag(u'def', {u'name': u'x()'}, (3, 5), [Text(u'\n        this is x\n    ', (3, 22))]), Text(u'\n    ', (5, 12)), DefTag(u'def', {u'name': u'y()'}, (6, 5), [Text(u'\n        this is y\n    ', (6, 22))]), Text(u'\n', (8, 12))]), Text(u'\n\n', (9, 14)), Code(u'\nresult = []\ndata = get_data()\nfor x in data:\n    result.append(x+7)\n\n', False, (11, 1)), Text(u'\n\n    result: ', (16, 3)), CallTag(u'call', {u'expr': u'foo.x(result)'}, (18, 13), []), Text(u'\n', (18, 42))])
+        )
 
     def test_expression(self):
         template = """
@@ -236,7 +325,10 @@ class LexerTest(TemplateTest):
         ${hi()}
 """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        this is some ', (1, 1)), Expression(u'text', [], (2, 22)), Text(u' and this is ', (2, 29)), Expression(u'textwith ', ['escapes', 'moreescapes'], (2, 42)), Text(u'\n        ', (2, 76)), DefTag(u'def', {u'name': u'hi()'}, (3, 9), ["Text(u'\\n            give me ', (3, 27))", "Expression(u'foo()', [], (4, 21))", "Text(u' and ', (4, 29))", "Expression(u'bar()', [], (4, 34))", "Text(u'\\n        ', (4, 42))"]), Text(u'\n        ', (5, 16)), Expression(u'hi()', [], (6, 9)), Text(u'\n', (6, 16))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        this is some ', (1, 1)), Expression(u'text', [], (2, 22)), Text(u' and this is ', (2, 29)), Expression(u'textwith ', ['escapes', 'moreescapes'], (2, 42)), Text(u'\n        ', (2, 76)), DefTag(u'def', {u'name': u'hi()'}, (3, 9), [Text(u'\n            give me ', (3, 27)), Expression(u'foo()', [], (4, 21)), Text(u' and ', (4, 29)), Expression(u'bar()', [], (4, 34)), Text(u'\n        ', (4, 42))]), Text(u'\n        ', (5, 16)), Expression(u'hi()', [], (6, 9)), Text(u'\n', (6, 16))])
+        )
         
 
     def test_tricky_expression(self):
@@ -245,36 +337,68 @@ class LexerTest(TemplateTest):
             ${x and "|" or "hi"}
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), Expression(u'x and "|" or "hi"', [], (3, 13)), Text(u'\n        ', (3, 33))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), Expression(u'x and "|" or "hi"', [], (3, 13)), Text(u'\n        ', (3, 33))])
+        )
 
         template = """
         
             ${hello + '''heres '{|}' text | | }''' | escape1}
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), Expression(u"hello + '''heres '{|}' text | | }''' ", ['escape1'], (3, 13)), Text(u'\n        ', (3, 62))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        \n            ', (1, 1)), Expression(u"hello + '''heres '{|}' text | | }''' ", ['escape1'], (3, 13)), Text(u'\n        ', (3, 62))])
+        )
 
     def test_tricky_code(self):
-        template = """<% print 'hi %>' %>"""
-        nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Code(u"print 'hi %>' \n", False, (1, 1))])"""
+        if util.py3k:
+            template = """<% print('hi %>') %>"""
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Code(u"print('hi %>') \n", False, (1, 1))])
+            )
+        else:
+            template = """<% print 'hi %>' %>"""
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Code(u"print 'hi %>' \n", False, (1, 1))])
+            )
         
-        template = r"""
-        <%
-            lines = src.split('\n')
-        %>
-"""
-        nodes = Lexer(template).parse()
-    
     def test_tricky_code_2(self):
         template = """<% 
         # someone's comment
         %>
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Code(u" \n        # someone's comment\n        \n", False, (1, 1)), Text(u'\n        ', (3, 11))])"""
-        
-        template= """<%
+        self._compare(
+            nodes,
+            TemplateNode({}, [Code(u" \n        # someone's comment\n        \n", False, (1, 1)), Text(u'\n        ', (3, 11))])
+        )
+
+    if util.py3k:
+        def test_tricky_code_3(self):
+            template= """<%
+            print('hi')
+            # this is a comment
+            # another comment
+            x = 7 # someone's '''comment
+            print('''
+        there
+        ''')
+            # someone else's comment
+        %> '''and now some text '''"""
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Code(u"\nprint('hi')\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint('''\n        there\n        ''')\n# someone else's comment\n        \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))])
+            )
+    else:
+        def test_tricky_code_3(self):
+            template= """<%
             print 'hi'
             # this is a comment
             # another comment
@@ -284,8 +408,11 @@ class LexerTest(TemplateTest):
         '''
             # someone else's comment
         %> '''and now some text '''"""
-        nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Code(u"\nprint 'hi'\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint '''\n        there\n        '''\n# someone else's comment\n        \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))])"""
+            nodes = Lexer(template).parse()
+            self._compare(
+                nodes,
+                TemplateNode({}, [Code(u"\nprint 'hi'\n# this is a comment\n# another comment\nx = 7 # someone's '''comment\nprint '''\n        there\n        '''\n# someone else's comment\n        \n", False, (1, 1)), Text(u" '''and now some text '''", (10, 11))])
+            )
         
     def test_control_lines(self):
         template = """
@@ -302,8 +429,10 @@ text text la la
     
 """
         nodes = Lexer(template).parse()
-        #print nodes
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\ntext text la la\n', (1, 1)), ControlLine(u'if', u'if foo():', False, (3, 1)), Text(u' mroe text la la blah blah\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'\n        and osme more stuff\n', (6, 1)), ControlLine(u'for', u'for l in range(1,5):', False, (8, 1)), Text(u'    tex tesl asdl l is ', (9, 1)), Expression(u'l', [], (9, 24)), Text(u' kfmas d\n', (9, 28)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'    tetx text\n    \n', (11, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\ntext text la la\n', (1, 1)), ControlLine(u'if', u'if foo():', False, (3, 1)), Text(u' mroe text la la blah blah\n', (4, 1)), ControlLine(u'if', u'endif', True, (5, 1)), Text(u'\n        and osme more stuff\n', (6, 1)), ControlLine(u'for', u'for l in range(1,5):', False, (8, 1)), Text(u'    tex tesl asdl l is ', (9, 1)), Expression(u'l', [], (9, 24)), Text(u' kfmas d\n', (9, 28)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'    tetx text\n    \n', (11, 1))])
+        )
 
     def test_control_lines_2(self):
         template = \
@@ -315,7 +444,10 @@ text text la la
 % endfor
 """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n\n\n', (1, 1)), ControlLine(u'for', u"for file in requestattr['toc'].filenames:", False, (4, 1)), Text(u'    x\n', (5, 1)), ControlLine(u'for', u'endfor', True, (6, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n\n\n', (1, 1)), ControlLine(u'for', u"for file in requestattr['toc'].filenames:", False, (4, 1)), Text(u'    x\n', (5, 1)), ControlLine(u'for', u'endfor', True, (6, 1))])
+        )
 
     def test_long_control_lines(self):
         template = \
@@ -326,7 +458,10 @@ text text la la
     % endfor
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'for', u"for file in \\\n        requestattr['toc'].filenames:", False, (2, 1)), Text(u'        x\n', (4, 1)), ControlLine(u'for', u'endfor', True, (5, 1)), Text(u'        ', (6, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'for', u"for file in \\\n        requestattr['toc'].filenames:", False, (2, 1)), Text(u'        x\n', (4, 1)), ControlLine(u'for', u'endfor', True, (5, 1)), Text(u'        ', (6, 1))])
+        )
 
     def test_unmatched_control(self):
         template = """
@@ -381,7 +516,10 @@ text text la la
         % endif
 """    
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x:', False, (2, 1)), Text(u'            hi\n', (3, 1)), ControlLine(u'elif', u'elif y+7==10:', False, (4, 1)), Text(u'            there\n', (5, 1)), ControlLine(u'elif', u'elif lala:', False, (6, 1)), Text(u'            lala\n', (7, 1)), ControlLine(u'else', u'else:', False, (8, 1)), Text(u'            hi\n', (9, 1)), ControlLine(u'if', u'endif', True, (10, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x:', False, (2, 1)), Text(u'            hi\n', (3, 1)), ControlLine(u'elif', u'elif y+7==10:', False, (4, 1)), Text(u'            there\n', (5, 1)), ControlLine(u'elif', u'elif lala:', False, (6, 1)), Text(u'            lala\n', (7, 1)), ControlLine(u'else', u'else:', False, (8, 1)), Text(u'            hi\n', (9, 1)), ControlLine(u'if', u'endif', True, (10, 1))])
+        )
         
     def test_integration(self):
         template = """<%namespace name="foo" file="somefile.html"/>
@@ -406,8 +544,10 @@ text text la la
 </table>
 """
         nodes = Lexer(template).parse()
-        expected = r"""TemplateNode({}, [NamespaceTag(u'namespace', {u'file': u'somefile.html', u'name': u'foo'}, (1, 1), []), Text(u'\n', (1, 46)), Comment(u'inherit from foobar.html', (2, 1)), InheritTag(u'inherit', {u'file': u'foobar.html'}, (3, 1), []), Text(u'\n\n', (3, 31)), DefTag(u'def', {u'name': u'header()'}, (5, 1), ["Text(u'\\n     <div>header</div>\\n', (5, 23))"]), Text(u'\n', (7, 8)), DefTag(u'def', {u'name': u'footer()'}, (8, 1), ["Text(u'\\n    <div> footer</div>\\n', (8, 23))"]), Text(u'\n\n<table>\n', (10, 8)), ControlLine(u'for', u'for j in data():', False, (13, 1)), Text(u'    <tr>\n', (14, 1)), ControlLine(u'for', u'for x in j:', False, (15, 1)), Text(u'            <td>Hello ', (16, 1)), Expression(u'x', ['h'], (16, 23)), Text(u'</td>\n', (16, 30)), ControlLine(u'for', u'endfor', True, (17, 1)), Text(u'    </tr>\n', (18, 1)), ControlLine(u'for', u'endfor', True, (19, 1)), Text(u'</table>\n', (20, 1))])"""
-        assert repr(nodes) == expected
+        self._compare(
+            nodes,
+            TemplateNode({}, [NamespaceTag(u'namespace', {u'file': u'somefile.html', u'name': u'foo'}, (1, 1), []), Text(u'\n', (1, 46)), Comment(u'inherit from foobar.html', (2, 1)), InheritTag(u'inherit', {u'file': u'foobar.html'}, (3, 1), []), Text(u'\n\n', (3, 31)), DefTag(u'def', {u'name': u'header()'}, (5, 1), [Text(u'\n     <div>header</div>\n', (5, 23))]), Text(u'\n', (7, 8)), DefTag(u'def', {u'name': u'footer()'}, (8, 1), [Text(u'\n    <div> footer</div>\n', (8, 23))]), Text(u'\n\n<table>\n', (10, 8)), ControlLine(u'for', u'for j in data():', False, (13, 1)), Text(u'    <tr>\n', (14, 1)), ControlLine(u'for', u'for x in j:', False, (15, 1)), Text(u'            <td>Hello ', (16, 1)), Expression(u'x', ['h'], (16, 23)), Text(u'</td>\n', (16, 30)), ControlLine(u'for', u'endfor', True, (17, 1)), Text(u'    </tr>\n', (18, 1)), ControlLine(u'for', u'endfor', True, (19, 1)), Text(u'</table>\n', (20, 1))])        
+        )
         
     def test_comment_after_statement(self):
         template = """
@@ -418,12 +558,18 @@ text text la la
         % endif #end
 """    
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x: #comment', False, (2, 1)), Text(u'            hi\n', (3, 1)), ControlLine(u'else', u'else: #next', False, (4, 1)), Text(u'            hi\n', (5, 1)), ControlLine(u'if', u'endif #end', True, (6, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n', (1, 1)), ControlLine(u'if', u'if x: #comment', False, (2, 1)), Text(u'            hi\n', (3, 1)), ControlLine(u'else', u'else: #next', False, (4, 1)), Text(u'            hi\n', (5, 1)), ControlLine(u'if', u'endif #end', True, (6, 1))])
+        )
 
     def test_crlf(self):
-        template = file(self._file_path("crlf.html")).read()
+        template = open(self._file_path("crlf.html"), 'rb').read()
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'<html>\r\n\r\n', (1, 1)), PageTag(u'page', {u'args': u"a=['foo',\n                'bar']"}, (3, 1), []), Text(u'\r\n\r\nlike the name says.\r\n\r\n', (4, 26)), ControlLine(u'for', u'for x in [1,2,3]:', False, (8, 1)), Text(u'        ', (9, 1)), Expression(u'x', [], (9, 9)), Text(u'', (9, 13)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'\r\n', (11, 1)), Expression(u"trumpeter == 'Miles' and trumpeter or \\\n      'Dizzy'", [], (12, 1)), Text(u'\r\n\r\n', (13, 15)), DefTag(u'def', {u'name': u'hi()'}, (15, 1), ["Text(u'\\r\\n    hi!\\r\\n', (15, 19))"]), Text(u'\r\n\r\n</html>\r\n', (17, 8))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'<html>\r\n\r\n', (1, 1)), PageTag(u'page', {u'args': u"a=['foo',\n                'bar']"}, (3, 1), []), Text(u'\r\n\r\nlike the name says.\r\n\r\n', (4, 26)), ControlLine(u'for', u'for x in [1,2,3]:', False, (8, 1)), Text(u'        ', (9, 1)), Expression(u'x', [], (9, 9)), Text(u'', (9, 13)), ControlLine(u'for', u'endfor', True, (10, 1)), Text(u'\r\n', (11, 1)), Expression(u"trumpeter == 'Miles' and trumpeter or \\\n      'Dizzy'", [], (12, 1)), Text(u'\r\n\r\n', (13, 15)), DefTag(u'def', {u'name': u'hi()'}, (15, 1), [Text(u'\r\n    hi!\r\n', (15, 19))]), Text(u'\r\n\r\n</html>\r\n', (17, 8))])
+        )
         assert flatten_result(Template(template).render()) == """<html> like the name says. 1 2 3 Dizzy </html>"""
     
     def test_comments(self):
@@ -447,7 +593,10 @@ comment
 hi
 """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n<style>\n #someselector\n # other non comment stuff\n</style>\n', (1, 1)), Comment(u'a comment', (6, 1)), Text(u'\n# also not a comment\n\n', (7, 1)), Comment(u'this is a comment', (10, 1)), Text(u'   \nthis is ## not a comment\n\n', (11, 1)), Comment(u' multiline\ncomment\n', (14, 1)), Text(u'\n\nhi\n', (16, 8))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n<style>\n #someselector\n # other non comment stuff\n</style>\n', (1, 1)), Comment(u'a comment', (6, 1)), Text(u'\n# also not a comment\n\n', (7, 1)), Comment(u'this is a comment', (10, 1)), Text(u'   \nthis is ## not a comment\n\n', (11, 1)), Comment(u' multiline\ncomment\n', (14, 1)), Text(u'\n\nhi\n', (16, 8))])
+        )
     
     def test_docs(self):
         template = """
@@ -461,7 +610,10 @@ hi
         </%def>
         """
         nodes = Lexer(template).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n        ', (1, 1)), Comment(u'\n            this is a comment\n        ', (2, 9)), Text(u'\n        ', (4, 16)), DefTag(u'def', {u'name': u'foo()'}, (5, 9), ["Text(u'\\n            ', (5, 28))", "Comment(u'\\n                this is the foo func\\n            ', (6, 13))", "Text(u'\\n        ', (8, 20))"]), Text(u'\n        ', (9, 16))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n        ', (1, 1)), Comment(u'\n            this is a comment\n        ', (2, 9)), Text(u'\n        ', (4, 16)), DefTag(u'def', {u'name': u'foo()'}, (5, 9), [Text(u'\n            ', (5, 28)), Comment(u'\n                this is the foo func\n            ', (6, 13)), Text(u'\n        ', (8, 20))]), Text(u'\n        ', (9, 16))])
+        )
 
     def test_preprocess(self):
         def preproc(text):
@@ -472,5 +624,8 @@ hi
 # another comment
 """
         nodes = Lexer(template, preprocessor=preproc).parse()
-        assert repr(nodes) == r"""TemplateNode({}, [Text(u'\n    hi\n', (1, 1)), Comment(u'old style comment', (3, 1)), Comment(u'another comment', (4, 1))])"""
+        self._compare(
+            nodes,
+            TemplateNode({}, [Text(u'\n    hi\n', (1, 1)), Comment(u'old style comment', (3, 1)), Comment(u'another comment', (4, 1))])
+        )
         
diff --git a/test/test_template.py b/test/test_template.py
index f06ab5b93c1c78d35307d8f623e838d876d46b92..970565a9555405066543722ec1ba5cc04400890c 100644
--- a/test/test_template.py
+++ b/test/test_template.py
@@ -3,7 +3,7 @@
 from mako.template import Template, ModuleTemplate
 from mako.lookup import TemplateLookup
 from mako.ext.preprocessors import convert_comments
-from mako import exceptions
+from mako import exceptions, util
 import re, os
 from util import flatten_result, result_lines
 import codecs
@@ -50,10 +50,13 @@ class EncodingTest(TemplateTest):
                     directories=[template_base], 
                     output_encoding='utf-8', 
                     default_filters=['decode.utf8'])
-        template = lookup.get_template('/chs_unicode.html')
+        if util.py3k:
+            template = lookup.get_template('/chs_unicode_py3k.html')
+        else:
+            template = lookup.get_template('/chs_unicode.html')
         eq_(
-            flatten_result(template.render(name='毛泽东')),
-            '毛泽东 是 新中国的主席<br/> Welcome 你 to 北京.'
+            flatten_result(template.render_unicode(name='毛泽东')),
+            u'毛泽东 是 新中国的主席<br/> Welcome 你 to 北京.'
         )
 
     def test_unicode_bom(self):
@@ -76,14 +79,14 @@ class EncodingTest(TemplateTest):
     def test_unicode_memory(self):
         val = u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"""
         self._do_memory_test(
-            "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'),
+            ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'),
             u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"""
         )
     
     def test_unicode_text(self):
         val = u"""<%text>Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »</%text>"""
         self._do_memory_test(
-            "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'),
+            ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'),
             u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"""
         )
 
@@ -96,19 +99,28 @@ class EncodingTest(TemplateTest):
         <%text>Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »</%text>
         </%call>"""
         self._do_memory_test(
-            "## -*- coding: utf-8 -*-\n" + val.encode('utf-8'),
+            ("## -*- coding: utf-8 -*-\n" + val).encode('utf-8'),
             u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
             filters=flatten_result
         )
         
     def test_unicode_literal_in_expr(self):
-        self._do_memory_test(
-            u"""## -*- coding: utf-8 -*-
-            ${u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"}
-            """.encode('utf-8'),
-            u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
-            filters = lambda s:s.strip()
-        )
+        if util.py3k:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                ${"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"}
+                """.encode('utf-8'),
+                u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
+                filters = lambda s:s.strip()
+            )
+        else:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                ${u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »"}
+                """.encode('utf-8'),
+                u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
+                filters = lambda s:s.strip()
+            )
 
     def test_unicode_literal_in_expr_file(self):
         self._do_file_test(
@@ -118,29 +130,54 @@ class EncodingTest(TemplateTest):
         )
 
     def test_unicode_literal_in_code(self):
-        self._do_memory_test(
-            u"""## -*- coding: utf-8 -*-
-            <%
-                context.write(u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »")
-            %>
-            """.encode('utf-8'),
-            u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
-            filters=lambda s:s.strip()
-        )
+        if util.py3k:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%
+                    context.write("Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »")
+                %>
+                """.encode('utf-8'),
+                u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
+                filters=lambda s:s.strip()
+            )
+        else:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%
+                    context.write(u"Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »")
+                %>
+                """.encode('utf-8'),
+                u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""",
+                filters=lambda s:s.strip()
+            )
     
     def test_unicode_literal_in_controlline(self):
-        self._do_memory_test(
-            u"""## -*- coding: utf-8 -*-
-            <%
-                x = u"drôle de petit voix m’a réveillé."
-            %>
-            % if x==u"drôle de petit voix m’a réveillé.":
-                hi, ${x}
-            % endif
-            """.encode('utf-8'),
-            u"""hi, drôle de petit voix m’a réveillé.""",
-            filters=lambda s:s.strip(),
-        )
+        if util.py3k:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%
+                    x = "drôle de petit voix m’a réveillé."
+                %>
+                % if x=="drôle de petit voix m’a réveillé.":
+                    hi, ${x}
+                % endif
+                """.encode('utf-8'),
+                u"""hi, drôle de petit voix m’a réveillé.""",
+                filters=lambda s:s.strip(),
+            )
+        else:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%
+                    x = u"drôle de petit voix m’a réveillé."
+                %>
+                % if x==u"drôle de petit voix m’a réveillé.":
+                    hi, ${x}
+                % endif
+                """.encode('utf-8'),
+                u"""hi, drôle de petit voix m’a réveillé.""",
+                filters=lambda s:s.strip(),
+            )
     
     def test_unicode_literal_in_tag(self):
         self._do_file_test(
@@ -155,7 +192,7 @@ class EncodingTest(TemplateTest):
         )
 
         self._do_memory_test(
-            file(self._file_path("unicode_arguments.html")).read(),
+            open(self._file_path("unicode_arguments.html"), 'rb').read(),
             [
                 u'x is: drôle de petit voix m’a réveillé',
                 u'x is: drôle de petit voix m’a réveillé',
@@ -166,45 +203,83 @@ class EncodingTest(TemplateTest):
         )
         
     def test_unicode_literal_in_def(self):
-        self._do_memory_test(
-            u"""## -*- coding: utf-8 -*-
-            <%def name="bello(foo, bar)">
-            Foo: ${ foo }
-            Bar: ${ bar }
-            </%def>
-            <%call expr="bello(foo=u'árvíztűrő tükörfúrógép', bar=u'ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
-            </%call>""".encode('utf-8'),
-            u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
-            filters=flatten_result
-        )
+        if util.py3k:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%def name="bello(foo, bar)">
+                Foo: ${ foo }
+                Bar: ${ bar }
+                </%def>
+                <%call expr="bello(foo='árvíztűrő tükörfúrógép', bar='ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
+                </%call>""".encode('utf-8'),
+                u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
+                filters=flatten_result
+            )
+
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%def name="hello(foo='árvíztűrő tükörfúrógép', bar='ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
+                Foo: ${ foo }
+                Bar: ${ bar }
+                </%def>
+                ${ hello() }""".encode('utf-8'),
+                u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
+                filters=flatten_result
+            )
+        else:
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%def name="bello(foo, bar)">
+                Foo: ${ foo }
+                Bar: ${ bar }
+                </%def>
+                <%call expr="bello(foo=u'árvíztűrő tükörfúrógép', bar=u'ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
+                </%call>""".encode('utf-8'),
+                u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
+                filters=flatten_result
+            )
         
-        self._do_memory_test(
-            u"""## -*- coding: utf-8 -*-
-            <%def name="hello(foo=u'árvíztűrő tükörfúrógép', bar=u'ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
-            Foo: ${ foo }
-            Bar: ${ bar }
-            </%def>
-            ${ hello() }""".encode('utf-8'),
-            u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
-            filters=flatten_result
-        )
+            self._do_memory_test(
+                u"""## -*- coding: utf-8 -*-
+                <%def name="hello(foo=u'árvíztűrő tükörfúrógép', bar=u'ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP')">
+                Foo: ${ foo }
+                Bar: ${ bar }
+                </%def>
+                ${ hello() }""".encode('utf-8'),
+                u"""Foo: árvíztűrő tükörfúrógép Bar: ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP""",
+                filters=flatten_result
+            )
         
     def test_input_encoding(self):
         """test the 'input_encoding' flag on Template, and that unicode 
             objects arent double-decoded"""
         
-        self._do_memory_test(
-            u"hello ${f(u'śląsk')}",
-            u"hello śląsk",
-            input_encoding='utf-8',
-            template_args={'f':lambda x:x}
-        )    
-
-        self._do_memory_test(
-            u"## -*- coding: utf-8 -*-\nhello ${f(u'śląsk')}",
-            u"hello śląsk",
-            template_args={'f':lambda x:x}
-        )
+        if util.py3k:
+            self._do_memory_test(
+                u"hello ${f('śląsk')}",
+                u"hello śląsk",
+                input_encoding='utf-8',
+                template_args={'f':lambda x:x}
+            )    
+
+            self._do_memory_test(
+                u"## -*- coding: utf-8 -*-\nhello ${f('śląsk')}",
+                u"hello śląsk",
+                template_args={'f':lambda x:x}
+            )
+        else:
+            self._do_memory_test(
+                u"hello ${f(u'śląsk')}",
+                u"hello śląsk",
+                input_encoding='utf-8',
+                template_args={'f':lambda x:x}
+            )    
+
+            self._do_memory_test(
+                u"## -*- coding: utf-8 -*-\nhello ${f(u'śląsk')}",
+                u"hello śląsk",
+                template_args={'f':lambda x:x}
+            )
 
     def test_raw_strings(self):
         """test that raw strings go straight thru with default_filters turned off"""
@@ -243,9 +318,13 @@ class EncodingTest(TemplateTest):
     def test_read_unicode(self):
         lookup = TemplateLookup(directories=[template_base], 
                                 filesystem_checks=True, output_encoding='utf-8')
-        template = lookup.get_template('/read_unicode.html')
+        if util.py3k:
+            template = lookup.get_template('/read_unicode_py3k.html')
+        else:
+            template = lookup.get_template('/read_unicode.html')
         data = template.render(path=self._file_path('internationalization.html'))
 
+    @skip_if(lambda: util.py3k)
     def test_bytestring_passthru(self):
         self._do_file_test(
             'chs_utf8.html',
@@ -418,7 +497,7 @@ class ControlTest(TemplateTest):
         t = Template("""
     ## this is a template.
     % for x in y:
-    %   if x.has_key('test'):
+    %   if 'test' in x:
         yes x has test
     %   else:
         no x does not have test
@@ -474,7 +553,7 @@ class RichTracebackTest(TemplateTest):
                 filename = 'unicode_syntax_error.html'
             else:
                 filename = 'unicode_runtime_error.html'
-            source = file(self._file_path(filename)).read()
+            source = open(self._file_path(filename), 'rb').read()
             if not utf8:
                 source = source.decode('utf-8')
             templateargs = {'filename':self._file_path(filename)}