Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assignment Expression Reports superfluous-parens False Positive #3249

Closed
t-mart opened this issue Nov 17, 2019 · 22 comments
Closed

Assignment Expression Reports superfluous-parens False Positive #3249

t-mart opened this issue Nov 17, 2019 · 22 comments
Labels
Assignment expression Related to the walrus operator / assignment expression Bug 🪲 Good first issue Friendly and approachable by new contributors Help wanted 🙏 Outside help would be appreciated, good for new contributors

Comments

@t-mart
Copy link

t-mart commented Nov 17, 2019

An if statement that tests a negated (not) parenthesized Assignment Expression without any other operators (See "Non-issues" section below) is reported by Pylint to have superfluous parentheses.

I believe this is a false positive. Removing the parentheses is invalid syntax.

Steps to reproduce

  1. Write an if statement that tests a negated (not) Assignment Expression, such as:
if not (my_var := False):
    print(my_var)
  1. Run pylint against that code.

Current behavior

Pylint reports superfluous-parens:Unnecessary parens after 'not' keyword.

Expected behavior

Pylint should not report about this syntax.

Removing the parentheses is a syntax error: SyntaxError: cannot use named assignment with operator.

pylint --version output

pylint 2.4.4
astroid 2.3.3
Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:21:23) [MSC v.1916 32 bit (Intel)]

Non-issues

  • If the negated assignment-expression if statement contains another binary operator, no Pylint report is generated about it. (Or at least, that seems to be the only difference.)

    For example, this slightly-modified snippet from What’s New In Python 3.8 does not cause Pylint to report:

    if not (n := len(list(range(11))) > 10:
        print(f"List is too long ({n} elements, expected <= 10)")
@PCManticore
Copy link
Contributor

Thanks for the report!

@t-mart
Copy link
Author

t-mart commented Nov 22, 2019

Ah, it seems this issue is more general than I first thought. The negation has nothing to do with it.

Example that also causes superfluous-parens:

if (foo := True):
    print(foo)

@huwcbjones
Copy link
Contributor

Ah, it seems this issue is more general than I first thought. The negation has nothing to do with it.

Example that also causes superfluous-parens:

if (foo := True):
    print(foo)

I would say that example is a legit case where we do want to raise this warning. In the example below, I believe the expected behaviour would be to raise a superfluous warning for 2, a syntax error for 3, and no warning for 1 & 4

# 1: Legal python, not superfluous
if foo := True:
    print(foo)

# 2: Legal python, superfluous
if (foo := True):
    print(foo)

# 3: Illegal python, not superfluous
if not foo := True:
    print(foo)

# 4: Legal python, "superfluous"
if not (foo := True):
    print(foo)

@t-mart
Copy link
Author

t-mart commented Feb 7, 2020

@huwcbjones Ah, thanks for going through some examples.

So, in a conditional, is there no correct way to negate a assignment expression (regarding examples 3 and 4)?

@huwcbjones
Copy link
Contributor

No worries, I came across this issue at work when I was trying to pylint some negated assignments and wondering why it was getting rejected!

So from what I can tell, you can definitely negate an assignment. In both 3 & 4, the print statement will not be executed, but after the conditional, foo will still have a value of True.

The "correct" way or, maybe more accurately, the only way to negate an assignment is to use wrap the assignment in brackets (as done in 4) however this leads to pylint complaining. But 3 throws a syntax error due to the difference in operator bindings (according to #3278).

So to answer your original question, yes this is a correct way to negate an assignment, however pylint should not report this - it is a false positive (as in the OP/title).

Coincidentally, I've found that the pattern of negating the assignment and early returning leads to cleaner.

if not (data := find_something()):
    log.warning("Could not find x")
    return
do_something_with_data(data)

Rather than

data = find_something()
if not data:
    log.warning("Could not find x")
    return
do_something_with_data(data)

@godlygeek
Copy link

This also applies to

assert (ret := str(42))

pylint gives superfluous-parens, but removing the parens is a syntax error.

@t-mart t-mart changed the title Negating Assignment Expression False Positive Assignment Expression Reports superfluous-parens False Positive Jun 9, 2020
@ViableSystemModel
Copy link

ViableSystemModel commented Jun 11, 2020

A couple more things. These apply to all conditionals, so while loops and assigned comparisons also give a false positive:

if not (a := True):
    pass

while not (a := True):
    pass

cond = not (a := True)

Meanwhile, in addition to 'not' and 'assert', the 'in' keyword also triggers a false positive. And in all cases where one of these keywords is present, the false positive is avoided when the assignment expression contains a comma within a pair of brackets or curly braces. I wrote the test cases for the 'in' keyword, then realized it was more general and didn't want to rewrite them. In other words:

# False positive (str, tuple)
A = [int(n) for n in (nums := '123')]
B = [int(n) for n in (nums := ('1', '2', '3'))]

# No false positive (list, set, dict)
C = [int(n) for n in (nums := ['1', '2', '3'])]
D = [int(n) for n in (nums := {'1', '2', '3'})]
E = [int(n) for n in (nums := {'1': 1, '2': 2, '3': 3})]

# False positive (brackets only)
F = [int(n) for n in (nums := ['1'])]
G = [int(n) for n in (nums := {'1'})]
H = [int(n) for n in (nums := {'1': 1})]
I = [int(n) for n in (nums := ('1'))]
J = [int(n) for n in (nums := ('1', ))]

# No false positive (brackets plus comma)
K = [int(n) for n in (nums := ['1', ])]
L = [int(n) for n in (nums := {'1', })]
M = [int(n) for n in (nums := {'1': 1, })]

# False positives (comma outside brackets)
N = [int(next(iter(n))) for n in (nums := (['1'], ['2'], ['3']))]
O = [int(next(iter(n))) for n in (nums := ({'1'}, {'2'}, {'3'}))]
P = [int(next(iter(n))) for n in (nums := ({'1': 1}, {'2': 2}, {'3': 3}))]

# False positives (aliasing)
NUMS_LIST = ['1', '2', '3']
NUMS_SET = {'1', '2', '3'}
NUMS_DICT = {'1': 1, '2': 2, '3': 3}
Q = [int(n) for n in (nums := NUMS_LIST)]
R = [int(n) for n in (nums := NUMS_SET)]
S = [int(n) for n in (nums := NUMS_DICT)]
T = [int(n) for n in (nums := [*NUMS_LIST])]
U = [int(n) for n in (nums := NUMS_LIST[:])]

# 2D arrays with numpy (since they allow commas within indexing)
import numpy
ARRAY_2D = numpy.zeroes((3, 3), dtype=bool)
V = not (vals := ARRAY_2D[:, :])  # No false positive
X = not (vals := ARRAY_2D[2, 2])  # No false positive
W = not (vals := ARRAY_2D[::])  # False positive
Y = not (vals := ARRAY_2D[2:2])  # False positive
Z = not (just_want_to_finish_the_alphabet := True)  # False positive

@nedbat
Copy link
Contributor

nedbat commented Aug 29, 2020

I came across this without a walrus operator:

assert ("Version " + __version__) in output

produces "Unnecessary parens after 'assert' keyword (superfluous-parens)"

@huwcbjones
Copy link
Contributor

@nedbat , that is because your sample is a case of superfluous params in Python 3.6.8, 3.7.2, 3.8.0 (all I had installed locally).

See output below:
$ python3
Python 3.7.2 (default, Feb  1 2019, 13:13:11)
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> __version__ = "1.2.3"
>>> output = "Version 1.2.3"
>>> assert ("Version " + __version__) in output
>>> assert "Version " + __version__ in output
>>> output = "foo"
>>> assert ("Version " + __version__) in output
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert "Version " + __version__ in output
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
The reason why this snippet is a true case of superfluous params is because the `+` operator binds tighter than the `in` operator.

A similar case is shown below. BODMAS (whatever flavour you use) says that multiplication binds tighter than addition, so the brackets are redundant (superfluous).
2 + (5 * 3) = 17 ≡ 2 + 5 * 3 = 17

Additionally this issue is about the assignment expression, which does not feature in your complaint.

@huwcbjones
Copy link
Contributor

However I believe this issue may have been fixed in #3697
@brycepg could you confirm this is the case?

@nedbat
Copy link
Contributor

nedbat commented Aug 30, 2020

@huwcbjones No, it's not about BODMAS. It's about the paren immediately following the assert keyword. In these lines, only the last one is given the violation:

x = ("Version " + __version__) in output 
y = 2 + (5 * 3)
assert "Version " + __version__ in output
assert "" + ("Version " + __version__) in output
assert ("Version " + __version__) in output

@huwcbjones
Copy link
Contributor

Ah, yes I see what you mean now. Just to make sure we're on the same page, all 5 of those statements should raise superfluous-parens?

However, my earlier point still stands, this is a different bug to the one reported in this thread as it does contain the assignment operator.

@nedbat
Copy link
Contributor

nedbat commented Aug 30, 2020

No, none of them should. The violation in question here is about writing assert as if it were a function:

assert(x == 1)

None of these examples do that. Pylint should not and does not complain about over-parenthesizing expressions.

@conqp
Copy link

conqp commented Sep 16, 2020

The same issue occurs on yield statements:

@classmethod
    def from_json(cls, json):
        """Yields records from a JSON-ish dict."""
        modules = json.pop('modules', None) or ()
        yield (order := super().from_json(json))

        for module in modules:
            yield OrderedModule(order=order, module=Module(module))

termorder/orm.py:147:0: C0325: Unnecessary parens after 'yield' keyword (superfluous-parens)

@RaenonX
Copy link

RaenonX commented Nov 23, 2020

However I believe this issue may have been fixed in #3697
@brycepg could you confirm this is the case?

I am using pylint 2.6.0 and I can confirm that this code still emitting a false-positive warning.

if not (foo := bar(7)):
   pass

Any fix?

@MatthewLennie
Copy link

I can also confirm tried Pylint 2.6.0 and 2.7.0 and in both cases was still able to get the false flag. Same case as @RaenonX

if not (val := bar(7)): pass

@RaenonX
Copy link

RaenonX commented May 20, 2021

Under the version listed below, this occasionally exists.

pylint 2.8.2
astroid 2.5.6
Python 3.9.1 (tags/v3.9.1:1e5d33e, Dec  7 2020, 17:08:21) [MSC v.1927 64 bit (AMD64)]

False positive / warning still appear

Example 1

if not (result := validate_conditions(conditions)):
    raise ConditionValidationFailedError(result)

Example 2

if not (lang_asset := self._assets.get(lang_code)):
    raise LanguageAssetNotFoundError(lang_code)

Example 3

# REMOVE: not with walrus https://github.com/PyCQA/pylint/issues/3249
if not (lang_entry := lang_asset.get(label)):
    if on_not_found is THROW_ERROR_ON_FAIL:
        raise TextLabelNotFoundError(label)

    return on_not_found

True positive / warning disappears

Example 1

if not (hit_label := self.assigned_hit_label):
    return None

Example 2

if not (hit_attr := asset_manager.asset_hit_attr.get_data_by_id(hit_label)):
    return None

Example 3

if not (enemy_param_id := data[variation_field]):
    continue

Example 4

if not (hit_attr_data := self._asset_hit_attr.get_data_by_id(hit_label)):
    continue

I have no idea why some warnings are gone while the others are not, so I am throwing all I have here. Hopefully this helps.

@Pierre-Sassoulas Pierre-Sassoulas added Good first issue Friendly and approachable by new contributors Help wanted 🙏 Outside help would be appreciated, good for new contributors labels May 21, 2021
@Pierre-Sassoulas Pierre-Sassoulas pinned this issue May 21, 2021
@Pierre-Sassoulas
Copy link
Member

Thanks, for the additional details and test cases. I pinned the issue.

@freud14
Copy link

freud14 commented Jun 23, 2021

Another example where there is a false positive:

[el
for lst in list_of_lists
for el in (lst if isinstance(lst, (list, tuple)) else [lst])]

The for l in list_of_lists is for context.

@Pierre-Sassoulas Pierre-Sassoulas unpinned this issue Jul 1, 2021
@RaenonX
Copy link

RaenonX commented Jul 5, 2021

Update:

The examples I provided above still has the issue.

Version info:

pylint 2.9.3
astroid 2.6.2
Python 3.9.5 (tags/v3.9.5:0a7dcbd, May  3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]

DanielNoord added a commit to DanielNoord/pylint that referenced this issue Aug 1, 2021
This fixes the false positives identified in pylint-dev#2818, pylint-dev#3249, pylint-dev#3608 & pylint-dev#4346
All false positives reported fell under keywords before walrus operator
or if-keyword within generators/comprehension.
This closes pylint-dev#2818, closes pylint-dev#3429, closes pylint-dev#3608, closes pylint-dev#4346
DanielNoord added a commit to DanielNoord/pylint that referenced this issue Aug 1, 2021
This fixes the false positives identified in pylint-dev#2818, pylint-dev#3249, pylint-dev#3608 & pylint-dev#4346
All false positives reported fell under keywords before walrus operator
or if-keyword within generators/comprehension.
This closes pylint-dev#2818, closes pylint-dev#3429, closes pylint-dev#3608, closes pylint-dev#4346
DanielNoord added a commit to DanielNoord/pylint that referenced this issue Aug 1, 2021
This fixes the false positives identified in pylint-dev#2818, pylint-dev#3249, pylint-dev#3608 & pylint-dev#4346
All false positives reported fell under keywords before walrus operator
or if-keyword within generators/comprehension.
This closes pylint-dev#2818, closes pylint-dev#3429, closes pylint-dev#3608, closes pylint-dev#4346
Pierre-Sassoulas added a commit that referenced this issue Aug 3, 2021
* Split functional tests for ``superfluous-parents``

* Fix false positives for superfluous-parens
This fixes the false positives identified in #2818, #3249, #3608 & #4346
All false positives reported fell under keywords before walrus operator
or if-keyword within generators/comprehension.
This closes #2818, closes #3429, closes #3608, closes #4346

* Move the superfluous functional tests to functional/s/super

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@DanielNoord
Copy link
Collaborator

@Pierre-Sassoulas For some reason this did not auto-close. I think #4792 has all test cases not covered by #4784, so I think this can be closed?

@Pierre-Sassoulas
Copy link
Member

Pierre-Sassoulas commented Aug 3, 2021

Closing because it's been mostly fixed in #4784 and the remaining use case are in #4792

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Assignment expression Related to the walrus operator / assignment expression Bug 🪲 Good first issue Friendly and approachable by new contributors Help wanted 🙏 Outside help would be appreciated, good for new contributors
Projects
None yet
Development

No branches or pull requests