Syntax Changes

Python 3 cleaned up some warts of the language’s syntax.

The changes needed to accommodate this are mostly mechanical, with little chance of breaking code, so they work well as the first patches to send to a project when intending to port it.

Tabs and Spaces

  • Fixer: see below
  • Prevalence: Very common (unless the code uses a style linter)

In Python 2, a tab character in indentation was considered equal to 8 spaces or less. In Python 3, a tab is only equal to another tab, so the following code is rejected (whitespace highlighted):

def f(cond):
····if cond:
→       do_something()
····else:
→       do_something_else()

If your code mixes tabs and spaces, the easiest way to fix this is converting all tabs to spaces. You can use the following Bash command for this:

find . -name '*.py' -type f -exec bash -c 'T=$(mktemp); expand -i -t 8 "$0" > "$T" && mv "$T" "$0"' {} \;

Tuple Unpacking in Parameter Lists

  • Fixer: python-modernize -wnf fissix.fixes.fix_tuple_params (fixup needed)
  • Prevalence: Common

Python 3 requires that each argument of a def function has a name. This simplifies code that uses introspection (such as help systems, documentation generation, and automatic dispatchers), but it does have a drawback: tuples are no longer allowed in formal parameter lists.

For example, functions like these are no longer allowed in Python 3:

def line((x1, y1), (x2, y2)):
    connect_points(Point(x1, y1), Point(x2, y2))

lambda (key, item): (item, key)

The recommended fixer does a good job in finding places that need fixing, but it does need some manual cleanup. The above example would be rewritten to:

def line(xxx_todo_changeme, xxx_todo_changeme1):
    (x1, y1) = xxx_todo_changeme
    (x2, y2) = xxx_todo_changeme1
    connect_points(Point(x1, y1), Point(x2, y2))

lambda key_item: (key_item[1], key_item[0])

For def, each of the newly introduced variables should be renamed to something more appropriate.

As for lambda, this transformation can leave the code less readable than before. For each such lambda, you should consider if replacing it with a regular named function would be an improvement.

Backticks

  • Fixer: python-modernize -wnf fissix.fixes.fix_repr (with caveat)
  • Prevalence: Common

The backtick (`) operator was removed in Python 3. It is confusingly similar to a single quote, and hard to type on some keyboards. Instead of the backtick, use the equivalent built-in function repr().

The recommended fixer does a good job, though it doesn’t catch the case where the name repr is redefined, as in:

repr = None
print(`1+2`)

which becomes:

repr = None
print(repr(1+2))

Re-defining built-in functions is usually considered bad style, but it never hurts to check if the code does it.

The Inequality Operator

  • Fixer: python-modernize -wnf fissix.fixes.fix_ne
  • Prevalence: Rare

In the spirit of “There’s only one way to do it”, Python 3 removes the little-known alternate spelling for inequality: the <> operator.

The recommended fixer will replace all occurrences with !=.

New Reserved Words

  • Fixer: None
  • Prevalence: Rare

Constants

In Python 3, None, True and False are syntactically keywords, not variable names, and cannot be assigned to. This was partially the case with None even in Python 2.6.

Hopefully, production code does not assign to True or False. If yours does, figure a way to do it differently.

async and await

Since Python 3.7, async and await are also keywords.

If your code uses these names, rename it. If other code depends on the names, keep the old name available for old Python versions. The way to do this will be different in each case, but generally you’ll need to take advantage of the fact that in Python’s various namespaces the strings 'async' and 'await' are still valid keys, even if they are not accesible usual with the syntax.

For module-level functions, classes and constants, also assign the original name using globals(). For example, a function previously named async could look like this:

def asynchronous():
    """...

    This function used to be called `async`.
    It is still available under old name.
    """

globals()['async'] = asynchronous

For methods, and class-level constants, assign the original name using setattr:

class MyClass:
    def asynchronous(self):
        """...

        This method used to be called `async`.
        It is still available under old name.
        """

setattr(MyClass, 'async', MyClass.asynchronous)

For function parameters, more work is required. The result will depend on whether the argument is optional and whether None is a valid value for it. Here is a general starting point:

def process_something(asynchronous=None, **kwargs):
    if asynchronous is None:
        asynchronous = kwargs.get('async', None)
    else:
        if 'async' in kwargs:
            raise TypeError('Both `asynchronous` and `async` specified')
    if asynchronous is None:
        raise TypeError('The argument `asynchronous` is required')

For function arguments, if the parameter cannot be renamed as above, use “double star” syntax that allows you to pass arbitrary argument names:

process_something(**{'async': True})

Other Syntax Changes

For convenience and completeness, this section lists syntax changes covered in other chapters: