97

Is it possible to have assignment in a condition?

For ex.

if (a=some_func()):
    # Use a
Vishal
  • 17,727
  • 17
  • 72
  • 91

10 Answers10

114

Why not try it out?

>>> def some_func():
...   return 2
... 
>>> if (a = some_func()):
  File "<stdin>", line 1
    if (a = some_func()):
          ^
SyntaxError: invalid syntax

So, no.

Update: This is possible (with different syntax) in Python 3.8

if a := some_func():
wjandrea
  • 16,334
  • 5
  • 30
  • 53
Jason Hall
  • 19,881
  • 4
  • 47
  • 56
  • 39
    this is intentionally forbidden as Guido, benevolent python dictator, finds them unnecessary and more confusing than useful. It's the same reason there's no post-increment or pre-increment operators (++). – Matt Boehm Apr 08 '10 at 22:53
  • 4
    he did allow the addition of *augmented assigment* in 2.0 because `x = x + 1` requires additional lookup time while `x += 1` was somewhat faster, but i'm sure he didn't even like doing *that* much. :-) – wescpy Apr 08 '10 at 23:56
  • hope, this will become possible soon. Python is just slow to evolve – Nik O'Lai Aug 28 '20 at 09:07
  • 12
    "why not try it out" - Because who knows what the syntax might be? Maybe OP tried that and it didn't work, but that doesn't mean the syntax isn't different, or that there's not a way to do it that's not intended – Levi H Sep 22 '20 at 17:09
63

UPDATE - Original answer is near the bottom

Python 3.8 will bring in PEP572

Abstract
This is a proposal for creating a way to assign to variables within an expression using the notation NAME := expr. A new exception, TargetScopeError is added, and there is one change to evaluation order.

https://lwn.net/Articles/757713/

The "PEP 572 mess" was the topic of a 2018 Python Language Summit session led by benevolent dictator for life (BDFL) Guido van Rossum. PEP 572 seeks to add assignment expressions (or "inline assignments") to the language, but it has seen a prolonged discussion over multiple huge threads on the python-dev mailing list—even after multiple rounds on python-ideas. Those threads were often contentious and were clearly voluminous to the point where many probably just tuned them out. At the summit, Van Rossum gave an overview of the feature proposal, which he seems inclined toward accepting, but he also wanted to discuss how to avoid this kind of thread explosion in the future.

https://www.python.org/dev/peps/pep-0572/#examples-from-the-python-standard-library

Examples from the Python standard library

site.py env_base is only used on these lines, putting its assignment on the if moves it as the "header" of the block.

Current:

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

Improved:

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

Avoid nested if and remove one indentation level.

Current:

if self._is_special:
    ans = self._check_nans(context=context)
    if ans:
        return ans

Improved:

if self._is_special and (ans := self._check_nans(context=context)):
    return ans

copy.py Code looks more regular and avoid multiple nested if. (See Appendix A for the origin of this example.)

Current:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error(
                "un(deep)copyable object of type %s" % cls)

Improved:

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(deep)copyable object of type %s" % cls)
datetime.py

tz is only used for s += tz, moving its assignment inside the if helps to show its scope.

Current:

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

Improved:

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

sysconfig.py Calling fp.readline() in the while condition and calling .match() on the if lines make the code more compact without

making it harder to understand.

Current:

while True:
    line = fp.readline()
    if not line:
        break
    m = define_rx.match(line)
    if m:
        n, v = m.group(1, 2)
        try:
            v = int(v)
        except ValueError:
            pass
        vars[n] = v
    else:
        m = undef_rx.match(line)
        if m:
            vars[m.group(1)] = 0

Improved:

while line := fp.readline():
    if m := define_rx.match(line):
        n, v = m.group(1, 2)
        try:
            v = int(v)
        except ValueError:
            pass
        vars[n] = v
    elif m := undef_rx.match(line):
        vars[m.group(1)] = 0

Simplifying list comprehensions A list comprehension can map and filter efficiently by capturing the condition:

results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]

Similarly, a subexpression can be reused within the main expression, by giving it a name on first use:

stuff = [[y := f(x), x/y] for x in range(5)]

Note that in both cases the variable y is bound in the containing scope (i.e. at the same level as results or stuff).

Capturing condition values Assignment expressions can be used to good effect in the header of an if or while statement:

# Loop-and-a-half
while (command := input("> ")) != "quit":
    print("You entered:", command)

# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
    print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

# Reading socket data until an empty string is returned
while data := sock.recv(8192):
    print("Received data:", data)

Particularly with the while loop, this can remove the need to have an infinite loop, an assignment, and a condition. It also creates a smooth parallel between a loop which simply uses a function call as its condition, and one which uses that as its condition but also uses the actual value.

Fork An example from the low-level UNIX world:

if pid := os.fork():
    # Parent code
else:
    # Child code

Original answer

http://docs.python.org/tutorial/datastructures.html

Note that in Python, unlike C, assignment cannot occur inside expressions. C programmers may grumble about this, but it avoids a common class of problems encountered in C programs: typing = in an expression when == was intended.

also see:

http://effbot.org/pyfaq/why-can-t-i-use-an-assignment-in-an-expression.htm

wjandrea
  • 16,334
  • 5
  • 30
  • 53
John La Rooy
  • 263,347
  • 47
  • 334
  • 476
  • 1
    I like this answer because it actually points out _why_ such a "feature" might have been deliberately left out of Python. When teaching beginner's programming, I have seen many make this mistake `if (foo = 'bar')` while intending to test the value of `foo`. – Jonathan Cross Feb 19 '19 at 23:25
  • 3
    @JonathanCross, This "feature" is actually going to be added in 3.8. It is unlikely to be used as sparingly as it should - but at least it is not a plain `=` – John La Rooy Feb 19 '19 at 23:52
  • @JohnLaRooy: Looking at the examples, I think "unlikely to be used as sparingly as it should" was spot-on; Out of the ~10 examples, I find that only two actually improve the code. (Namely, as the sole expression either in a while condition, to avoid duplicating the line or having the loop condition in the body, or in an elif-chain to avoid nesting) – Aleksi Torhamo Apr 10 '20 at 11:07
  • Doesn't work with ternary `a if a := 1 == 1 else False` → `Invalid syntax` :c So apparently still have to resort to the old awkward `(lambda: (ret := foo(), ret if ret else None))()[-1]` *(i.e. as a way to avoid the repeated call to `foo()` in ternary)*. – Hi-Angel Nov 25 '20 at 21:20
39

Nope, the BDFL didn't like that feature.

From where I sit, Guido van Rossum, "Benevolent Dictator For Life”, has fought hard to keep Python as simple as it can be. We can quibble with some of the decisions he's made -- I'd have preferred he said 'No' more often. But the fact that there hasn't been a committee designing Python, but instead a trusted "advisory board", based largely on merit, filtering through one designer's sensibilities, has produced one hell of a nice language, IMHO.

Kevin Little
  • 11,232
  • 5
  • 35
  • 46
  • 17
    Simple? This feature could simplified quite some of my code because it could have made it more compact and therefor more readable. Now I need two lines where I used to need one. I never got the point why Python rejected features other programming languages have for many years (and often for a very good reason). Especially this feature we're talking about here is very, very useful. – Regis May Sep 10 '17 at 08:09
  • 6
    Less code isn't always simpler or morde readable. Take a recursive function for example. It's loop-equivalent is often more readable. – F.M.F. Apr 09 '18 at 08:50
  • 1
    I don't like like the C version of it, but I really miss having something like rust's `if let` when I have an if elif chain, but need to store and use the value of the condition in each case. – Thayne Jul 10 '18 at 04:57
  • 1
    I have to say the code I am writing now (the reason I searched this issue) is MUCH uglier without this feature. Instead of using if followed by lots of else ifs, I need to keep indenting the next if under the last else. – MikeKulls Oct 24 '18 at 03:34
17

Not directly, per this old recipe of mine -- but as the recipe says it's easy to build the semantic equivalent, e.g. if you need to transliterate directly from a C-coded reference algorithm (before refactoring to more-idiomatic Python, of course;-). I.e.:

class DataHolder(object):
    def __init__(self, value=None): self.value = value
    def set(self, value): self.value = value; return value
    def get(self): return self.value

data = DataHolder()

while data.set(somefunc()):
  a = data.get()
  # use a

BTW, a very idiomatic Pythonic form for your specific case, if you know exactly what falsish value somefunc may return when it does return a falsish value (e.g. 0), is

for a in iter(somefunc, 0):
  # use a

so in this specific case the refactoring would be pretty easy;-).

If the return could be any kind of falsish value (0, None, '', ...), one possibility is:

import itertools

for a in itertools.takewhile(lambda x: x, iter(somefunc, object())):
    # use a

but you might prefer a simple custom generator:

def getwhile(func, *a, **k):
    while True:
      x = func(*a, **k)
      if not x: break
      yield x

for a in getwhile(somefunc):
    # use a
Alex Martelli
  • 762,786
  • 156
  • 1,160
  • 1,345
  • I would vote this up twice if I could. This is a great solution for those times when something like this is really needed. I adapted your solution to a regex Matcher class, which is instantiated once and then .check() is used in the if statement and .result() used inside its body to retrieve the match, if there was one. Thanks! :) – Teekin Jul 26 '18 at 15:23
16

Yes, but only from Python 3.8 and onwards.

PEP 572 proposes Assignment Expressions and has already been accepted.

Quoting the Syntax and semantics part of the PEP:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
   process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]

In your specific case, you will be able to write

if a := some_func():
    # Use a
timgeb
  • 64,821
  • 18
  • 95
  • 124
5

No. Assignment in Python is a statement, not an expression.

Ignacio Vazquez-Abrams
  • 699,552
  • 132
  • 1,235
  • 1,283
  • And Guido wouldn't have it any other way. – Mark Ransom Apr 08 '10 at 22:51
  • 1
    @MarkRansom All hail Guido. *Right* .. sigh. – StephenBoesch Dec 11 '17 at 04:49
  • @javadba the guy has been right much more often than he's been wrong. I appreciate that having a single person in charge of the vision results in a much more coherent strategy than design by committee; I can compare and contrast with C++ which is my main bread and butter. – Mark Ransom Dec 11 '17 at 04:58
  • I feel both ruby and scala ( v different languages) get it right significantly moreso than python: but in any case here is not the place.. – StephenBoesch Dec 11 '17 at 06:11
  • @MarkRansom *"And Guido wouldn't have it any other way"* - and yet, 10 years later we have [Assignment Expressions](https://www.python.org/dev/peps/pep-0572/)... – Tomerikoo Apr 10 '21 at 14:48
  • @Tomerikoo but Guido stepped down as BDFL so I think it's still true. – Mark Ransom Apr 10 '21 at 14:51
4

Thanks to Python 3.8 new feature it will be possible to do such a thing from this version, although not using = but Ada-like assignment operator :=. Example from the docs:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match
Jean-François Fabre
  • 126,787
  • 22
  • 103
  • 165
2

You can define a function to do the assigning for you:

def assign(name, value):
    import inspect
    frame = inspect.currentframe()
    try:
        locals_ = frame.f_back.f_locals
    finally:
        del frame 
    locals_[name] = value
    return value

if assign('test', 0):
    print("first", test)
elif assign('xyz', 123):
    print("second", xyz)
Willem Hengeveld
  • 2,569
  • 19
  • 19
1

One of the reasons why assignments are illegal in conditions is that it's easier to make a mistake and assign True or False:

some_variable = 5

# This does not work
# if True = some_variable:
#   do_something()

# This only works in Python 2.x
True = some_variable

print True  # returns 5

In Python 3 True and False are keywords, so no risk anymore.

1

The assignment operator - also known informally as the the walrus operator - was created at 28-Feb-2018 in PEP572.

For the sake of completeness, I'll post the relevant parts so you can compare the differences between 3.7 and 3.8:

3.7
---
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*

3.8
---
if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite]
namedexpr_test: test [':=' test]                         <---- WALRUS OPERATOR!!!
test: or_test ['if' or_test 'else' test] | lambdef
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
BPL
  • 9,807
  • 7
  • 37
  • 90