Skip to content

[pyupgrade] Properly trigger super change in nested class (UP008)#22677

Merged
charliermarsh merged 5 commits intoastral-sh:mainfrom
leandrobbraga:bugfix/up008-inner-class
Mar 10, 2026
Merged

[pyupgrade] Properly trigger super change in nested class (UP008)#22677
charliermarsh merged 5 commits intoastral-sh:mainfrom
leandrobbraga:bugfix/up008-inner-class

Conversation

@leandrobbraga
Copy link
Copy Markdown
Contributor

@leandrobbraga leandrobbraga commented Jan 18, 2026

While debugging, I noticed that the function parameters were of type Expr::Name in the normal case and Expr::Attribute when the function was in a nested class. Reading the documentation, it seems that being an Expr::Attribute makes sense because Inner is an attribute of Outer.

I did some thinking and could not identify any additional Expr types I should be pattern-matching here, but I would like some input from you.

The change seems to fix the issue without any regression.

echo 'class Base:
      def __init__(self, foo):
          self.foo = foo


  class Outer(Base):
      def __init__(self, foo):
          super().__init__(foo)  # Should not trigger UP008

      class Inner(Base):
          def __init__(self, foo):
              super().__init__(foo)  # Should not trigger UP008

      class InnerInner(Base):
          def __init__(self, foo):
              super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008

  ' | cargo run -p ruff -- check --select UP008 -
UP008 Use `super()` instead of `super(__class__, self)`
  --> -:16:18
   |
14 |     class InnerInner(Base):
15 |         def __init__(self, foo):
16 |             super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
help: Remove `super()` parameters

Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).

Closes #22597

@leandrobbraga leandrobbraga changed the title [pyupgrade] properly trigger UP008 in nested class [pyupgrade] properly trigger in nested class (UP008) Jan 18, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Jan 18, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@ntBre ntBre self-requested a review January 20, 2026 22:48
@ntBre ntBre added bug Something isn't working rule Implementing or modifying a lint rule labels Jan 20, 2026
@leandrobbraga
Copy link
Copy Markdown
Contributor Author

leandrobbraga commented Jan 20, 2026

This bug seems easy to introduce into the codebase. I did a quick search and found another example.

PLC1802 won’t trigger if the list is an attribute.

https://play.ruff.rs/f9f8f76b-c468-4f4f-a3b8-0bca5850daf1

class Fruits:
    fruits: list[str]

    def __init__(self, fruits: list[str]):
        self.fruits = fruits

fruits_class = Fruits(["orange", "apple"])
fruits_list = ["a", "b"]

if len(fruits_class.fruits):
    ...

if len(fruits_list):
    ...

I opened an issue and I can't think on how to prevent this class of bug.


class Outer(Base):
def __init__(self, foo):
super(self).__init__(foo) # Should not trigger UP008
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you intended here with the 1-argument super(self) call, but it actually raises an error at runtime:

Python 3.14.2 (tags/v3.14.2:df79316, Dec  5 2025, 17:18:21) [MSC v.1944 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> class Base:
...     def __init__(self, foo):
...         self.foo = foo
...
>>> class Outer(Base):
...     def __init__(self, foo):
...         super(self).__init__(foo)  # Should not trigger UP008
...
>>> o = Outer(5)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    o = Outer(5)
  File "<python-input-1>", line 3, in __init__
    super(self).__init__(foo)  # Should not trigger UP008
    ~~~~~^^^^^^
TypeError: super() argument 1 must be a type, not Outer

Perhaps you wanted to use either a 2-argument or 0-argument call instead? Also, if the super call doesn't have two arguments, the lint will return early, so I don't know if you wanted to verify that there is no panic in that case, for example?

// For a `super` invocation to be unnecessary, the first argument needs to match
// the enclosing class, and the second argument needs to match the first
// argument to the enclosing function.
let [first_arg, second_arg] = &*call.arguments.args else {
return;
};

Copy link
Copy Markdown
Contributor Author

@leandrobbraga leandrobbraga Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was supposed to be with 0-argument, thanks for pointing it.

@generalmimon
Copy link
Copy Markdown

generalmimon commented Jan 21, 2026

@leandrobbraga (#22677 (comment)):

echo 'class Base:
      def __init__(self, foo):
          self.foo = foo


  class Outer(Base):
      def __init__(self, foo):
          super().__init__(foo)  # Should not trigger UP008

      class Inner(Base):
          def __init__(self, foo):
              super().__init__(foo)  # Should not trigger UP008

      class InnerInner(Base):
          def __init__(self, foo):
              super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008

  ' | cargo run -p ruff -- check --select UP008 -
UP008 Use `super()` instead of `super(__class__, self)`
  --> -:16:18
   |
14 |     class InnerInner(Base):
15 |         def __init__(self, foo):
16 |             super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
help: Remove `super()` parameters

Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).

This doesn't work either - I suppose the indentation isn't what you intended?

$ cat test.py
class Base:
    def __init__(self, foo):
        self.foo = foo


class Outer(Base):
    def __init__(self, foo):
        super().__init__(foo)  # Should not trigger UP008

    class Inner(Base):
        def __init__(self, foo):
            super().__init__(foo)  # Should not trigger UP008

    class InnerInner(Base):
        def __init__(self, foo):
            super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008


o = Outer(5)
i = Outer.Inner(5)
ii = Outer.InnerInner(5)

$ python3 --version
Python 3.14.2

$ python3 test.py
Traceback (most recent call last):
  File "/home/pp/test.py", line 21, in <module>
    ii = Outer.InnerInner(5)
  File "/home/pp/test.py", line 16, in __init__
    super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008
          ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: type object 'Inner' has no attribute 'InnerInner'

@leandrobbraga
Copy link
Copy Markdown
Contributor Author

leandrobbraga commented Jan 21, 2026

This was a copy-paste indentation error, for some reason I lost the indentation when I copy-pasted, but it was tested properly.

Try linting this code:

class Base:
    def __init__(self, foo):
        self.foo = foo


class Outer(Base):
    def __init__(self, foo):
        super().__init__(foo)  # Should not trigger UP008

    class Inner(Base):
        def __init__(self, foo):
            super(Outer.Inner, self).__init__(foo)  # Should trigger UP008

        class InnerInner(Base):
            def __init__(self, foo):
                super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008

oii = Outer.Inner.InnerInner(1)

Output:

> echo 'class Base:
      def __init__(self, foo):
          self.foo = foo


  class Outer(Base):
      def __init__(self, foo):
          super().__init__(foo)  # Should not trigger UP008

      class Inner(Base):
          def __init__(self, foo):
              super(Outer.Inner, self).__init__(foo)  # Should trigger UP008

          class InnerInner(Base):
              def __init__(self, foo):
                  super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008

  oii = Outer.Inner.InnerInner(1)' | cargo run -p ruff -- check --select UP008 -
UP008 Use `super()` instead of `super(__class__, self)`
  --> -:12:18
   |
10 |     class Inner(Base):
11 |         def __init__(self, foo):
12 |             super(Outer.Inner, self).__init__(foo)  # Should trigger UP008
   |                  ^^^^^^^^^^^^^^^^^^^
13 |
14 |         class InnerInner(Base):
   |
help: Remove `super()` parameters

UP008 Use `super()` instead of `super(__class__, self)`
  --> -:16:22
   |
14 |         class InnerInner(Base):
15 |             def __init__(self, foo):
16 |                 super(Outer.Inner.InnerInner, self).__init__(foo)  # Should trigger UP008

Comment on lines 136 to 140
let second_arg_id = match second_arg {
Expr::Name(ast::ExprName { id, .. }) => id,
Expr::Attribute(ast::ExprAttribute { attr, .. }) => &attr.id,
_ => return,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of accepting an attribute in the second argument? This second_arg_id is required to be equal to the name of the "parent argument" (parent_arg), which is typically self:

&& second_arg_id == parent_arg.name().as_str())

I think it doesn't make sense to accept wild stuff like super(..., a.b.c.self).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, I removed the match on Expr::Attribute for the second parameter.

Remove the `Expr::Attribute` matching for the second parameter in
`super`. The UP008 does not care when it's an Attribute, the only cases
are:
- super(MyClass, self)
- super(OuterClass.InnerClass, self)

Which is always an `Expr::Name`
@leandrobbraga leandrobbraga force-pushed the bugfix/up008-inner-class branch from 0cf6617 to a9b58e1 Compare January 21, 2026 11:10
// Simple case: super(MyClass, self)
Expr::Name(ast::ExprName { id, .. }) => id,
// Nested class case: super(OuterClass.InnerClass, self)
Expr::Attribute(ast::ExprAttribute { attr, .. }) => &attr.id,
Copy link
Copy Markdown

@generalmimon generalmimon Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that whenever super(A.B, self) appears inside a class named B, it must be the super(__class__, self) case and an upgrade can be performed. Unfortunately, this is not necessarily true. Consider this:

class Base:
    def __init__(self, foo):
        print(f"Base.__init__({foo}) called")
        self.foo = foo


class Outer:
    class Inner(Base):
        def __init__(self, foo):
            print(f"Outer.Inner.__init__({foo}) called")
            super().__init__(foo)


class Inner(Outer.Inner):
    def __init__(self, foo):
        super(Outer.Inner, self).__init__(foo)


i = Inner(5)

Note that the super call in the Inner.__init__ method body is super(Outer.Inner, self), not super(Inner, self). If you run the above code, the output is the following:

root@d2a45e4529f7:~# python3 --version
Python 3.13.5
root@d2a45e4529f7:~# python3 test.py
Base.__init__(5) called

As you can see, only Base.__init__ was called, not Outer.Inner.__init__.

Nevertheless, your current implementation at a9b58e1 still suggests omitting arguments to this super call:

root@d2a45e4529f7:~/ruff# git status
On branch bugfix/up008-inner-class
Your branch is up to date with 'origin/bugfix/up008-inner-class'.

nothing to commit, working tree clean
root@d2a45e4529f7:~/ruff# git rev-parse HEAD
a9b58e1d3610ef14430569af480d9a5fd0b4d209
root@d2a45e4529f7:~/ruff# cargo run -p ruff -- check --select UP ../test.py --no-cache
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/ruff check --select UP ../test.py --no-cache`
UP008 Use `super()` instead of `super(__class__, self)`
  --> /root/test.py:16:14
   |
14 | class Inner(Outer.Inner):
15 |     def __init__(self, foo):
16 |         super(Outer.Inner, self).__init__(foo)
   |              ^^^^^^^^^^^^^^^^^^^
   |
help: Remove `super()` parameters

Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).

In preview, this is even offered as a safe fix:

root@d2a45e4529f7:~/ruff# cargo run -p ruff -- check --preview --select UP ../test.py --no-cache
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/ruff check --preview --select UP ../test.py --no-cache`
UP008 [*] Use `super()` instead of `super(__class__, self)`
  --> /root/test.py:16:14
   |
14 | class Inner(Outer.Inner):
15 |     def __init__(self, foo):
16 |         super(Outer.Inner, self).__init__(foo)
   |              ^^^^^^^^^^^^^^^^^^^
   |
help: Remove `super()` parameters
13 |
14 | class Inner(Outer.Inner):
15 |     def __init__(self, foo):
   -         super(Outer.Inner, self).__init__(foo)
16 +         super().__init__(foo)
17 |
18 |
19 | i = Inner(5)

Found 1 error.
[*] 1 fixable with the `--fix` option.

However, if you apply it, the behavior of the program changes - notice the additional message Outer.Inner.__init__(5) called, which the original program did not print:

root@d2a45e4529f7:~# cat test.py
class Base:
    def __init__(self, foo):
        print(f"Base.__init__({foo}) called")
        self.foo = foo


class Outer:
    class Inner(Base):
        def __init__(self, foo):
            print(f"Outer.Inner.__init__({foo}) called")
            super().__init__(foo)


class Inner(Outer.Inner):
    def __init__(self, foo):
        super().__init__(foo)


i = Inner(5)
root@d2a45e4529f7:~# python3 test.py
Outer.Inner.__init__(5) called
Base.__init__(5) called

I admit that this is perhaps an unusual scenario. Calling super(Outer.Inner, self) in the Inner class looks rather unintentional or at least suspicious, but from Python's point of view, it is a valid program that does not raise any runtime errors.

So I believe that Ruff should verify whether the entire attribute chain (in this case Outer.Inner) matches, not just its last segment. This means that instead of just finding the closest enclosing class:

// Find the enclosing class definition (if any).
let Some(Stmt::ClassDef(ast::StmtClassDef {
name: parent_name,
decorator_list,
..
})) = parents.find(|stmt| stmt.is_class_def_stmt())
else {
return;
};

... and comparing its name with the last segment in the attribute chain:

if !(((first_arg_id == "__class__") || (first_arg_id == parent_name.as_str()))

..., Ruff should also inspect further (nested) enclosing classes and compare the second-to-last segment in the attribute chain, third-to-last segment, etc. If anything doesn't match, UP008 should not be triggered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review, I'll try to think about the corner cases and come up with a solution in the next days.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review, I'll try to think about the corner cases and come up with a solution in the next days.

Copy link
Copy Markdown

@generalmimon generalmimon Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #22597 (comment) - I've just found out that the situation I described above is already covered in this pyupgrade test case:

asottile / pyupgrade / tests/features/super_test.py:32-35

        'class Outer:\n'  # super arg1 nested in unrelated name
        '    class C(Base):\n'
        '        def f(self):\n'
        '            super(some_module.Outer.C, self).f()\n',

Honestly, I think it's worth looking at the entire tests/features/super_test.py file and ideally porting all test cases that are not yet covered by the Ruff test suite. There aren't that many, but Ruff still seems to be missing a few important ones.

Copy link
Copy Markdown
Member

@amyreese amyreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a rebase and updated snapshots.

@charliermarsh charliermarsh changed the title [pyupgrade] properly trigger in nested class (UP008) [pyupgrade] Properly trigger super change in nested class (UP008) Mar 10, 2026
@charliermarsh charliermarsh merged commit f2dd40e into astral-sh:main Mar 10, 2026
41 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UP008 false negative in inner (nested) classes

5 participants