[Python-Dev] PEP 572: intended scope of assignment expression

Victor Stinner vstinner at redhat.com
Thu Jul 5 11:42:13 EDT 2018


After "Assignment expression and coding style: the while True case",
here is the part 2: analysis of the "if (var := expr): ..." case.

2018-07-05 14:20 GMT+02:00 Victor Stinner <vstinner at redhat.com>:
> *intended* scope.

I generated the giant pull request #8116 to show where I consider that
"if (var := expr): ..." would be appropriate in the stdlib:

   https://github.com/python/cpython/pull/8116/files

In short, replace:

    var = expr
    if var:
        ...

with:

    if (var := expr):
        ...


I used a script to replace "var = expr; if var: ..." with "if (var :=
expr): ...". I restricted my change to the simplest test "if var:",
other conditions like "if var > 0:" are left unchaned to keep this
change reviewable (short enough). The change is already big enough (62
files modified) to have enough examples! Then I validated each change
manually:

(*) I reverted all changes when 'var' is still used after the if.

(*) I also reverted some changes like "var = regex.match(); if var:
return var.group(1)", since it's already handled by my PR 8097:
https://github.com/python/cpython/pull/8097/files

(*) Sometimes, 'var' is only used in the condition and so has been
removed in this change. Example:

        ans = self._compare_check_nans(other, context)
        if ans:
            return False
        return self._cmp(other) < 0

replaced with:

        if self._compare_check_nans(other, context):
            return False
        return self._cmp(other) < 0

(Maybe such changes should be addressed in a different pull request.)


Below, some examples where I consider that assignment expressions give
a value to the reader.


== Good: site (straighforward) ==

    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base

replaced with:

    if (env_base := os.environ.get("PYTHONUSERBASE", None)):
        return env_base

Note: env_base is only used inside the if block.

== Good: datetime (more concise code) ==

New code:

    def isoformat(self, timespec='auto'):
        s = _format_time(self._hour, self._minute, self._second,
                          self._microsecond, timespec)
        if (tz := self._tzstr()):
            s += tz
        return s

This example shows the benefit of the PEP 572: remove one line without
making the code worse to read.

== Good: logging.handlers ==

    def close(self):
        self.acquire()
        try:
            sock = self.sock
            if sock:
                self.sock = None
                sock.close()
            logging.Handler.close(self)
        finally:
            self.release()

replaced with:

    def close(self):
        self.acquire()
        try:
            if (sock := self.sock):
                self.sock = None
                sock.close()
            logging.Handler.close(self)
        finally:
            self.release()

== Good: doctest ==

New code:

    # Deal with exact matches possibly needed at one or both ends.
    startpos, endpos = 0, len(got)
    if (w := ws[0]):   # starts with exact match
        if got.startswith(w):
            startpos = len(w)
            del ws[0]
        else:
            return False

    if (w := ws[-1]):   # ends with exact match
        if got.endswith(w):
            endpos -= len(w)
            del ws[-1]
        else:
            return False

    ...

This example is interesting: the 'w' variable is reused, but ":="
announces to the reader that the w is only intended to be used in one
if block.

== Good: csv (reuse var) ==

New code:

    n = groupindex['quote'] - 1
    if (key := m[n]):
        quotes[key] = quotes.get(key, 0) + 1
    try:
        n = groupindex['delim'] - 1
        key = m[n]
    except KeyError:
        continue
    if key and (delimiters is None or key in delimiters):
        delims[key] = delims.get(key, 0) + 1

As for doctest: "key := ..." shows that this value is only used in one
if block, but later key is reassigned to a new value.

== Good: difflib ==

New code using (isjunk := self.isjunk):

    # Purge junk elements
    self.bjunk = junk = set()
    if (isjunk := self.isjunk):
        for elt in b2j.keys():
            if isjunk(elt):
                junk.add(elt)
        for elt in junk: # separate loop avoids separate list of keys
            del b2j[elt]

-*-*-*-

== Borderline? sre_parse (two conditions) ==

    code = CATEGORIES.get(escape)
    if code and code[0] is IN:
        return code

replaced with:

    if (code := CATEGORIES.get(escape)) and code[0] is IN:
        return code

The test "code[0] is IN" uses 'code' just after it's defined on the
same line. Maybe it is surprising me, since I'm not sure to assignment
expressions yet.

-*-*-*-

== BAD! argparse (use after if) ==

Ok, now let's see cases where I consider that assignment expressions
are inappropriate! Here is a first example.

    help = self._root_section.format_help()
    if help:
        help = self._long_break_matcher.sub('\n\n', help)
        help = help.strip('\n') + '\n'
    return help

'help' is used after the if block.

== BAD! fileinput (use after if) ==

    line = self._readline()
    if line:
        self._filelineno += 1
        return line
    if not self._file:
        return line

'line' is used after the first if block.

== BAD! _osx_support (reuse var, use after if) ==

def _supports_universal_builds():
    osx_version = _get_system_version()
    if osx_version:
        try:
            osx_version = tuple(int(i) for i in osx_version.split('.'))
        except ValueError:
            osx_version = ''
    return bool(osx_version >= (10, 4)) if osx_version else False

Surprising reusage of the 'osx_version' variable, using ":=" would
more the code even more confusing (and osx_version is used after the
if).

Victor


More information about the Python-Dev mailing list