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

Ruff picks wrong scope for variable #4486

Closed
notatallshaw opened this issue May 18, 2023 · 8 comments · Fixed by #4494
Closed

Ruff picks wrong scope for variable #4486

notatallshaw opened this issue May 18, 2023 · 8 comments · Fixed by #4494
Assignees
Labels
bug Something isn't working

Comments

@notatallshaw
Copy link

notatallshaw commented May 18, 2023

Specifically, Ruff incorrectly identifies the scope a variable is chosen from when it's inside a comprehension inside a class inside a function and a variable with the same name exists inside the class scope.

Here is shortest code to reproduce this example, I named it scopetest.py:

def f():
    b = 'Function Scope'
    class C:
        b = 'Class Scope'
        d = [b for _ in range(1)]
    return C

print(f().d[0])  # Prints "Function Scope"

Running Ruff:

> ruff --version
ruff 0.0.267
> ruff scopetest.py
scopetest.py:2:5: F841 [*] Local variable `b` is assigned to but never used
Found 1 error.

If I run ruff with --fix it deletes the line that b is sourced from causing the code to now throw an exception.

The reason b is scoping from the function scope and not the class scope is that generators / comprehensions implicitly create an anonymous function and source their scope from locals, non-locals, globals, and builtins and skip the class scope as all other functions do.

I came across this testing different versions of code posted here where the Steering Council has decided to keep the behavior as-is for at least Python 3.12.

@charliermarsh charliermarsh added the bug Something isn't working label May 18, 2023
@charliermarsh
Copy link
Member

Interesting, thank you for filing.

@charliermarsh charliermarsh self-assigned this May 18, 2023
@charliermarsh
Copy link
Member

Do you understand why this runs without error?

class A:
    T = range(10)

    Z = (x for x in T)
    L = [x for x in T]
    B = dict((i, str(i)) for i in T)

@charliermarsh
Copy link
Member

Ah, so this scoping only applies to the generator body, and not the iterable.

@notatallshaw
Copy link
Author

notatallshaw commented May 18, 2023

Ah, so this scoping only applies to the generator body, and not the iterable.

Yes, your example initially threw me off, but I think in this case the iterable can be thought of like the function signature (e.g. the defaults of the arguments), it's scope is in the outer area and the body is like a function body.

@charliermarsh
Copy link
Member

Here's another interesting wrinkle -- this does not work:

class A:
    T = range(10)

    L = [x for x in T for y in T]

I noticed this via the comment in Carl Meyer's finder tool.

@charliermarsh
Copy link
Member

So it's only the first generator (?) that's evaluated in the parent scope.

@notatallshaw
Copy link
Author

So it's only the first generator (?) that's evaluated in the parent scope.

Right, expanding my analogy same how the signature of bar fails here:

class A:
    T = range(10)

    def foo(x=T):
        def bar(y=T):
            pass
        return bar()
    foo()

@charliermarsh
Copy link
Member

Thank you, that's a really helpful model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants