Skip to content

Walrus operator causes different behavior in PyTest. #10743

Closed

Description

@veritas9872

I am currently testing the following function but find that the function passes the test in the normal Python terminal but fails in a PyTest run. The walrus operator is relatively new and not many people use it. I think that there may be an inconsistency in the execution environment.

import numpy as np

def test_walrus_conversion():
    a = np.random.random(16)
    assert not np.array_equal(a, a := a.astype(np.uint8))
    assert np.all(a == 0)

Activity

  1. added
    type: bugproblem that needs to be addressed
    topic: rewriterelated to the assertion rewrite mechanism
    needs backportapplied to PRs, indicates that it should be ported to the current bug-fix branch
    on Feb 16, 2023
  2. RonnyPfannschmidt commented on Feb 16, 2023

    @RonnyPfannschmidt
    Member

    It's very likely a missed case in the asset rewrite

  3. aless10 commented on Feb 19, 2023

    @aless10
    Contributor

    Hi, I did some tests on this since I was curious about how this works.
    I created a module like this

    def my_func(before, after):
        return before == after
    
    def change_value(value):
        return value.lower()
    
    def test_walrus_conversion():
        a = "Hello"
        assert not my_func(a, a := change_value(a))
        assert a == "hello"
    

    and run it. It fails as the issuer stated. Then I added """PYTEST_DONT_REWRITE""" at the beginning of the module and now it passes.
    If it's ok with you I'd like to try to fix this.

    As a side note, this is a very edge case and I don't know if it is clean to write something like this. Anyway, since the unittest mode says that this test should pass, we should align with that

  4. RonnyPfannschmidt commented on Feb 19, 2023

    @RonnyPfannschmidt
    Member

    @aless10 thnks for providing a self-contained reproducer

    i wonder if it also behaves that way if change_value is inlined

    we should align with whatever is the result when using --assert=plain

  5. aless10 commented on Feb 22, 2023

    @aless10
    Contributor

    Quick update: I think I fixed the bug by telling the AssertionRewrite to visit the NamedExpr expr and not by using the generic visitor.
    That said, I got into another possible problem, because right now the error messages are not what we expect. Here is an example

    def test_walrus_conversion_fails():
        a = "Hello"
        assert a == (a := a.lower())
    

    this test fails correctly, but this is the message I've got

        def test_walrus_conversion_fails():
            a = "Hello"
    >       assert a == (a := a.lower())
    E       AssertionError: assert 'hello' == 'hello'
    E           hello
    
    

    and this is not what we expect, it's even worse! I'm trying to fix this behavior and I succeeded, but then this other test

    def test_walrus_conversion_fails():
        a = "Hello"
        assert a != (a := a.lower())
        assert a == 'hello'
    
    

    fails with this message

    >       assert a == 'hello'
    E       AssertionError: assert 'Hello' == 'hello'
    E         - hello
    E         ? ^
    E         + Hello
    E         ? ^
    
    

    , because the solution I found does not remember that we change the variable value.
    Another bit on this topic. It looks like there is something strange on how this thing works with mutable objects. For example

    def change_me(d: dict) -> dict:
        d['change'] = True
        return d
            
    def my_func(before, after):
        return before == after
                
    def test_change_me():
        a = {}
        assert my_func(a, change_me(a))
        assert a == {"change": True}
    

    this test has the same behavior in pytest and unittest, but it looks like the same pattern. In fact, there is a variable that is passed to a function with another argument that changes its value.
    To me these two tests below

    def change_me(d: dict) -> dict:
        d['change'] = True
        return d
    
    def change_me_str(value: str) -> str:
        return value.lower()
            
    def my_func(before, after):
        return before == after
    
    def test_walrus_conversion():
        a = "Hello"
        assert not my_func(a, a := change_me_str(a))
        assert a == "hello"
    
    
    def test_change_me():
        a = {}
        assert my_func(a, change_me(a))
        assert a == {"change": True}
    ````
    
    should behave the same, but in the first case (with the walrus operator) the change is not applied until the function is executed. 
    Ok that strings are immutable and dict aren't but it's very strange to understand and explain. 
    
  6. RonnyPfannschmidt commented on Feb 22, 2023

    @RonnyPfannschmidt
    Member

    Aliasing is important to consider, assertion side effects can hide deep changes, which is acceptable (just normal python)

    We might have to evaluate when to get a repr

    We better warn that we only capture references, not deep mutable state and strongly recommend doing assertion on immutable without side effects

  7. added a commit that references this issue on Mar 10, 2023
    6e478b0
  8. added 2 commits that reference this issue on Apr 18, 2026
    c783ee2
    879f594
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs backportapplied to PRs, indicates that it should be ported to the current bug-fix branchtopic: rewriterelated to the assertion rewrite mechanismtype: bugproblem that needs to be addressed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions