| 1 | ====================== |
| 2 | Python 3 compatibility |
| 3 | ====================== |
| 4 | |
| 5 | Django 1.5 introduces a compatibility layer that allows the code to be run both |
| 6 | in Python 2 (2.6/2.7) and Python 3 (>= 3.2) (*work in progress*). |
| 7 | |
| 8 | This document is not meant as a complete Python 2 to Python 3 migration guide. |
| 9 | There are many existing resources you can read. But we describe some utilities |
| 10 | and guidelines that we recommend you should use when you want to ensure your |
| 11 | code can be run with both Python 2 and 3. |
| 12 | |
| 13 | * http://docs.python.org/py3k/howto/pyporting.html |
| 14 | * http://python3porting.com/ |
| 15 | |
| 16 | django.utils.py3 |
| 17 | ================ |
| 18 | |
| 19 | Whenever a symbol or module has different semantics or different locations on |
| 20 | Python 2 and Python 3, you can import it from ``django.utils.py3`` where it |
| 21 | will be automatically converted depending on your current Python version. |
| 22 | |
| 23 | PY3 |
| 24 | --- |
| 25 | |
| 26 | If you need to know anywhere in your code if you are running Python 3 or a |
| 27 | previous Python 2 version, you can check the ``PY3`` boolean variable:: |
| 28 | |
| 29 | from django.utils.py3 import PY3 |
| 30 | |
| 31 | if PY3: |
| 32 | # Do stuff Python 3-wise |
| 33 | else: |
| 34 | # Do stuff Python 2-wise |
| 35 | |
| 36 | String handling |
| 37 | =============== |
| 38 | |
| 39 | In Python 3, all strings are considered Unicode strings by default. Byte strings |
| 40 | have to be prefixed with the letter 'b'. To mimic the same behaviour in Python 2, |
| 41 | we recommend you import ``unicode_literals`` from the ``__future__`` library:: |
| 42 | |
| 43 | from __future__ import unicode_literals |
| 44 | |
| 45 | my_string = "This is an unicode literal" |
| 46 | my_bytestring = b"This is a bytestring" |
| 47 | |
| 48 | Be cautious if you have to slice bytestrings. |
| 49 | See http://docs.python.org/py3k/howto/pyporting.html#bytes-literals |
| 50 | |
| 51 | Different expected strings |
| 52 | -------------------------- |
| 53 | |
| 54 | Some method parameters have changed the expected string type of a parameter. |
| 55 | For example, ``strftime`` format parameter expects a bytestring on Python 2 but |
| 56 | a normal (Unicode) string on Python 3. For these cases, ``django.utils.py3`` |
| 57 | provides a ``n()`` function which encodes the string parameter only with |
| 58 | Python 2. |
| 59 | |
| 60 | >>> from __future__ import unicode_literals |
| 61 | >>> from datetime import datetime |
| 62 | |
| 63 | >>> print(datetime.date(2012, 5, 21).strftime(n("%m → %Y"))) |
| 64 | 05 → 2012 |
| 65 | |
| 66 | Renamed types |
| 67 | ============= |
| 68 | |
| 69 | Several types are named differently in Python 2 and Python 3. In order to keep |
| 70 | compatibility while using those types, import their corresponding aliases from |
| 71 | ``django.utils.py3``. |
| 72 | |
| 73 | =========== ========= ===================== |
| 74 | Python 2 Python 3 django.utils.py3 |
| 75 | =========== ========= ===================== |
| 76 | basestring, str, string_types (tuple) |
| 77 | unicode str text_type |
| 78 | int, long int, integer_types (tuple) |
| 79 | long int long_type |
| 80 | =========== ========= ===================== |
| 81 | |
| 82 | String aliases |
| 83 | -------------- |
| 84 | |
| 85 | Code sample:: |
| 86 | |
| 87 | if isinstance(foo, basestring): |
| 88 | print("foo is a string") |
| 89 | |
| 90 | # I want to convert a number to a Unicode string |
| 91 | bar = 45 |
| 92 | bar_string = unicode(bar) |
| 93 | |
| 94 | Should be replaced by:: |
| 95 | |
| 96 | from django.utils.py3 import string_types, text_type |
| 97 | |
| 98 | if isinstance(foo, string_types): |
| 99 | print("foo is a string") |
| 100 | |
| 101 | # I want to convert a number to a Unicode string |
| 102 | bar = 45 |
| 103 | bar_string = text_type(bar) |
| 104 | |
| 105 | No more long type |
| 106 | ----------------- |
| 107 | ``long`` and ``int`` types have been unified in Python 3, meaning that ``long`` |
| 108 | is no more available. ``django.utils.py3`` provides both ``long_type`` and |
| 109 | ``integer_types`` aliases. For example: |
| 110 | |
| 111 | .. code-block:: python |
| 112 | |
| 113 | # Old Python 2 code |
| 114 | my_var = long(333463247234623) |
| 115 | if isinstance(my_var, (int, long)): |
| 116 | # ... |
| 117 | |
| 118 | Should be replaced by: |
| 119 | |
| 120 | .. code-block:: python |
| 121 | |
| 122 | from django.utils.py3 import long_type, integer_types |
| 123 | |
| 124 | my_var = long_type(333463247234623) |
| 125 | if isinstance(my_var, integer_types): |
| 126 | # ... |
| 127 | |
| 128 | |
| 129 | Changed module locations |
| 130 | ======================== |
| 131 | |
| 132 | The following modules have changed their location in Python 3. Therefore, it is |
| 133 | recommended to import them from the ``django.utils.py3`` compatibility layer: |
| 134 | |
| 135 | =============================== ====================================== ====================== |
| 136 | Python 2 Python3 django.utils.py3 |
| 137 | =============================== ====================================== ====================== |
| 138 | Cookie http.cookies cookies |
| 139 | |
| 140 | urlparse.urlparse urllib.parse.urlparse urlparse |
| 141 | urlparse.urlunparse urllib.parse.urlunparse urlunparse |
| 142 | urlparse.urljoin urllib.parse.urljoin urljoin |
| 143 | urlparse.urlsplit urllib.parse.urlsplit urlsplit |
| 144 | urlparse.urlunsplit urllib.parse.urlunsplit urlunsplit |
| 145 | urlparse.urldefrag urllib.parse.urldefrag urldefrag |
| 146 | urlparse.parse_qsl urllib.parse.parse_qsl parse_qsl |
| 147 | urllib.quote urllib.parse.quote quote |
| 148 | urllib.unquote urllib.parse.unquote unquote |
| 149 | urllib.quote_plus urllib.parse.quote_plus quote_plus |
| 150 | urllib.unquote_plus urllib.parse.unquote_plus unquote_plus |
| 151 | urllib.urlencode urllib.parse.urlencode urlencode |
| 152 | urllib.urlopen urllib.request.urlopen urlopen |
| 153 | urllib.url2pathname urllib.request.url2pathname url2pathname |
| 154 | urllib.urlretrieve urllib.request.urlretrieve urlretrieve |
| 155 | urllib2 urllib.request urllib2 |
| 156 | urllib2.Request urllib.request.Request Request |
| 157 | urllib2.OpenerDirector urllib.request.OpenerDirector OpenerDirector |
| 158 | urllib2.UnknownHandler urllib.request.UnknownHandler UnknownHandler |
| 159 | urllib2.HTTPHandler urllib.request.HTTPHandler HTTPHandler |
| 160 | urllib2.HTTPSHandler urllib.request.HTTPSHandler HTTPSHandler |
| 161 | urllib2.HTTPDefaultErrorHandler urllib.request.HTTPDefaultErrorHandler HTTPDefaultErrorHandler |
| 162 | urllib2.FTPHandler urllib.request.FTPHandler FTPHandler |
| 163 | urllib2.HTTPError urllib.request.HTTPError HTTPError |
| 164 | urllib2.HTTPErrorProcessor urllib.request.HTTPErrorProcessor HTTPErrorProcessor |
| 165 | |
| 166 | htmlentitydefs.name2codepoint html.entities.name2codepoint name2codepoint |
| 167 | HTMLParser html.parser HTMLParser |
| 168 | cPickle/pickle pickle pickle |
| 169 | thread/dummy_thread _thread/_dummy_thread thread |
| 170 | |
| 171 | os.getcwdu os.getcwd getcwdu |
| 172 | itertools.izip zip zip |
| 173 | sys.maxint sys.maxsize maxsize |
| 174 | unichr chr unichr |
| 175 | xrange range xrange |
| 176 | =============================== ====================================== ====================== |
| 177 | |
| 178 | |
| 179 | Ouptut encoding now Unicode |
| 180 | =========================== |
| 181 | |
| 182 | If you want to catch stdout/stderr output, the output content is UTF-8 encoded |
| 183 | in Python 2, while it is Unicode strings in Python 3. You can use the OutputIO |
| 184 | stream to capture this output:: |
| 185 | |
| 186 | from django.utils.py3 import OutputIO |
| 187 | |
| 188 | try: |
| 189 | old_stdout = sys.stdout |
| 190 | out = OutputIO() |
| 191 | sys.stdout = out |
| 192 | # Do stuff which produces standard output |
| 193 | result = out.getvalue() |
| 194 | finally: |
| 195 | sys.stdout = old_stdout |
| 196 | |
| 197 | Dict iteritems/itervalues/iterkeys |
| 198 | ================================== |
| 199 | |
| 200 | The iteritems(), itervalues() and iterkeys() methods of dictionaries do not |
| 201 | exist any more in Python 3, simply because they represent the default items() |
| 202 | values() and keys() behavior in Python 3. Therefore, to keep compatibility, |
| 203 | use similar functions from ``django.utils.py3``:: |
| 204 | |
| 205 | from django.utils.py3 import iteritems, itervalues, iterkeys |
| 206 | |
| 207 | my_dict = {'a': 21, 'b': 42} |
| 208 | for key, value in iteritems(my_dict): |
| 209 | # ... |
| 210 | for value in itervalues(my_dict): |
| 211 | # ... |
| 212 | for key in iterkeys(my_dict): |
| 213 | # ... |
| 214 | |
| 215 | Note that in Python 3, dict.keys(), dict.items() and dict.values() return |
| 216 | "views" instead of lists. Wrap them into list() if you really need their return |
| 217 | values to be in a list. |
| 218 | |
| 219 | http://docs.python.org/release/3.0.1/whatsnew/3.0.html#views-and-iterators-instead-of-lists |
| 220 | |
| 221 | Metaclass |
| 222 | ========= |
| 223 | |
| 224 | The syntax for declaring metaclasses has changed in Python 3. |
| 225 | ``django.utils.py3`` offers a compatible way to declare metaclasses:: |
| 226 | |
| 227 | from django.utils.py3 import with_metaclass |
| 228 | |
| 229 | class MyClass(with_metaclass(SubClass1, SubClass2,...)): |
| 230 | # ... |
| 231 | |
| 232 | Re-raising exceptions |
| 233 | ===================== |
| 234 | |
| 235 | One of the syntaxes to raise exceptions (raise E, V, T) is gone in Python 3. |
| 236 | This is especially used in very specific cases where you want to re-raise a |
| 237 | different exception that the initial one, while keeping the original traceback. |
| 238 | So, instead of:: |
| 239 | |
| 240 | raise Exception, Exception(msg), traceback |
| 241 | |
| 242 | Use:: |
| 243 | |
| 244 | from django.utils.py3 import reraise |
| 245 | |
| 246 | reraise(Exception, Exception(msg), traceback) |
| 247 | |