diff --git a/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html b/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html
new file mode 100644
index 000000000000..e742af8b3367
--- /dev/null
+++ b/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html
@@ -0,0 +1,5 @@
+
+
+This inspection detects invalid definition of __slots__ in a class.
+
+
\ No newline at end of file
diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml
index f1415df9cf21..29665e00b4df 100644
--- a/python/src/META-INF/python-core-common.xml
+++ b/python/src/META-INF/python-core-common.xml
@@ -383,7 +383,7 @@
-
+
diff --git a/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt b/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt
new file mode 100644
index 000000000000..3eaad119ed3b
--- /dev/null
+++ b/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.inspections
+
+import com.intellij.codeInspection.LocalInspectionToolSession
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.psi.PsiElementVisitor
+import com.jetbrains.python.PyNames
+import com.jetbrains.python.psi.*
+import com.jetbrains.python.psi.impl.PyPsiUtils
+
+class PyDunderSlotsInspection : PyInspection() {
+
+ override fun buildVisitor(holder: ProblemsHolder,
+ isOnTheFly: Boolean,
+ session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session)
+
+ private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) {
+
+ override fun visitPyClass(node: PyClass?) {
+ if (node != null && LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON30)) {
+ val slots = findSlotsValue(node)
+
+ when (slots) {
+ is PySequenceExpression -> slots
+ .elements
+ .asSequence()
+ .filterIsInstance()
+ .forEach {
+ processSlot(node, it)
+ }
+ is PyStringLiteralExpression -> processSlot(node, slots)
+ }
+ }
+ }
+
+ private fun findSlotsValue(pyClass: PyClass): PyExpression? {
+ val target = pyClass.findClassAttribute(PyNames.SLOTS, false, myTypeEvalContext) as? PyTargetExpression
+ val value = target?.findAssignedValue()
+
+ return PyPsiUtils.flattenParens(value)
+ }
+
+ private fun processSlot(pyClass: PyClass, slot: PyStringLiteralExpression) {
+ val name = slot.stringValue
+
+ if (pyClass.findClassAttribute(name, false, myTypeEvalContext) != null) {
+ registerProblem(slot, "'$name' in __slots__ conflicts with class variable")
+ }
+ }
+ }
+}
+
diff --git a/python/testData/inspections/PyDunderSlotsInspection/test.py b/python/testData/inspections/PyDunderSlotsInspection/test.py
new file mode 100644
index 000000000000..25e8864f649d
--- /dev/null
+++ b/python/testData/inspections/PyDunderSlotsInspection/test.py
@@ -0,0 +1,62 @@
+# PY-20280: one slot in list
+class Foo(object):
+ __slots__ = ['foo']
+ foo = 1
+
+
+# PY-20280: one slot in tuple
+class Foo(object):
+ __slots__ = ('foo')
+ foo = 1
+
+
+# PY-20280: one slot
+class Foo(object):
+ __slots__ = 'foo'
+ foo = 1
+
+
+# PY-20280: two slots in list
+class Foo(object):
+ __slots__ = ['foo', 'bar']
+ foo = 1
+
+
+# PY-20280: two slots in tuple
+class Foo(object):
+ __slots__ = ('foo', 'bar')
+ foo = 1
+
+
+# PY-20280: slots in base and class variable in derived
+class Base(object):
+ __slots__ = 'foo'
+
+class Derived(Base):
+ foo = 1
+
+
+# PY-20280: class variable in base and slots in derived
+class Base(object):
+ foo = 1
+
+class Derived(Base):
+ __slots__ = 'foo'
+
+
+# PY-20280: slots in base and derived, class variable in derived
+class Base(object):
+ __slots__ = 'foo'
+
+class Derived(Base):
+ __slots__ = 'bar'
+ foo = 1
+
+
+# PY-20280: slots in base and derived, class variable in base
+class Base(object):
+ __slots__ = 'bar'
+ foo = 1
+
+class Derived(Base):
+ __slots__ = 'foo'
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
index 26a1791d4720..da69bbbdfa4d 100644
--- a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
+++ b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
@@ -328,4 +328,8 @@ public class PythonInspectionsTest extends PyTestCase {
public void testPyShadowingNamesInspection() {
doHighlightingTest(PyShadowingNamesInspection.class);
}
+
+ public void testPyDunderSlotsInspection() {
+ runWithLanguageLevel(LanguageLevel.PYTHON30, () -> doHighlightingTest(PyDunderSlotsInspection.class));
+ }
}