diff --git a/python/python-psi-impl/resources/inspectionDescriptions/PyInvalidEscapeSequenceInspection.html b/python/python-psi-impl/resources/inspectionDescriptions/PyInvalidEscapeSequenceInspection.html
new file mode 100644
index 000000000000..636b26326d4f
--- /dev/null
+++ b/python/python-psi-impl/resources/inspectionDescriptions/PyInvalidEscapeSequenceInspection.html
@@ -0,0 +1,22 @@
+
+
+Reports invalid escape sequences in string and bytes literals.
+
+
+ Starting from Python 3.6, escape sequences that are not recognized (such as \.) produce a SyntaxWarning.
+ In future Python versions, they will become a SyntaxError.
+
+
+ To fix this, you can either escape the backslash (e.g., \\.) or use a raw string (e.g., r'\.').
+
+Example:
+
+# Warning: Invalid escape sequence '\.'
+print('\.')
+
+# Correct
+print(r'\.')
+print('\\.')
+
+
+
diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
index 9a7d8241b640..c4cf06f0c13c 100644
--- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml
+++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml
@@ -269,6 +269,7 @@
+
Python
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index 124daeb941f2..294879ab951b 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -1378,3 +1378,11 @@ QFIX.remove.cast.call=Remove 'cast' call
QFIX.add.source.root.notification.text=''{0}'' has been marked as source root
QFIX.add.source.root.notification.ok=Ok
QFIX.add.source.root.notification.revert=Revert
+
+# PyInvalidEscapeSequenceInspection
+INSP.NAME.invalid.escape.sequence=Invalid escape sequence
+INSP.invalid.escape.sequence=Invalid escape sequence ''{0}''
+
+# Quick fixes for PyInvalidEscapeSequenceInspection
+QFIX.escape.backslash=Escape backslash
+QFIX.convert.to.raw.string=Convert to raw string
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspection.kt
new file mode 100644
index 000000000000..f0dd82471475
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspection.kt
@@ -0,0 +1,91 @@
+package com.jetbrains.python.inspections
+
+import com.intellij.codeInspection.LocalInspectionToolSession
+import com.intellij.codeInspection.LocalQuickFix
+import com.intellij.codeInspection.ProblemHighlightType
+import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.openapi.util.TextRange
+import com.intellij.psi.PsiElementVisitor
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.inspections.quickfix.PyConvertToRawStringQuickFix
+import com.jetbrains.python.inspections.quickfix.PyEscapeBackslashQuickFix
+import com.jetbrains.python.psi.PyFormattedStringElement
+import com.jetbrains.python.psi.PyStringElement
+import com.jetbrains.python.psi.PyStringLiteralExpression
+import com.jetbrains.python.psi.impl.PyStringLiteralDecoder
+import com.jetbrains.python.psi.types.TypeEvalContext
+
+class PyInvalidEscapeSequenceInspection : PyInspection() {
+
+ override fun buildVisitor(
+ holder: ProblemsHolder,
+ isOnTheFly: Boolean,
+ session: LocalInspectionToolSession,
+ ): PsiElementVisitor = Visitor(holder, PyInspectionVisitor.getContext(session))
+
+ private class Visitor(holder: ProblemsHolder?, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
+
+ override fun visitPyStringLiteralExpression(node: PyStringLiteralExpression) =
+ node.stringElements.forEach { processString(it, node) }
+
+ private fun processString(element: PyStringElement, node: PyStringLiteralExpression) {
+ if (element.prefix.contains("r", ignoreCase = true)) return
+
+ val content = element.contentRange.substring(element.text)
+ val offsetInNode = element.textRange.startOffset + element.contentRange.startOffset - node.textRange.startOffset
+
+ val fstringRanges = if (element is PyFormattedStringElement) {
+ element.literalPartRanges.asSequence().map { it.shiftLeft(element.contentRange.startOffset) }
+ }
+ else {
+ sequenceOf(TextRange(0, content.length))
+ }
+
+ val validEscapes = fstringRanges.flatMap { range ->
+ PyStringLiteralDecoder.PATTERN_ESCAPE.toRegex()
+ .findAll(content, range.startOffset)
+ .takeWhile { it.range.last < range.endOffset }
+ .map { it.range }
+ }
+ .iterator()
+ val hasValidEscapes = validEscapes.hasNext()
+ var curValidEscape = if (hasValidEscapes) validEscapes.next() else null
+
+ for (range in fstringRanges) {
+ var charIndex = range.startOffset
+ while (charIndex < range.endOffset - 1) {
+
+ if (content[charIndex] != '\\') {
+ charIndex++
+ continue
+ }
+
+ while (curValidEscape?.let { it.last < charIndex } == true) {
+ curValidEscape = if (validEscapes.hasNext()) validEscapes.next() else null
+ }
+ if (curValidEscape?.contains(charIndex) == true) {
+ charIndex = curValidEscape.last + 1
+ continue
+ }
+
+ val problemOffset = offsetInNode + charIndex
+
+ val fixes = LocalQuickFix.notNullElements(
+ PyEscapeBackslashQuickFix(problemOffset),
+ if (!hasValidEscapes) PyConvertToRawStringQuickFix(problemOffset) else null
+ )
+
+ registerProblem(
+ node,
+ PyPsiBundle.message("INSP.invalid.escape.sequence", "\\${content[charIndex + 1]}"),
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ null,
+ TextRange(problemOffset, problemOffset + 2),
+ *fixes
+ )
+ charIndex += 2
+ }
+ }
+ }
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyConvertToRawStringQuickFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyConvertToRawStringQuickFix.kt
new file mode 100644
index 000000000000..9fe6203d4e75
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyConvertToRawStringQuickFix.kt
@@ -0,0 +1,28 @@
+package com.jetbrains.python.inspections.quickfix
+
+import com.intellij.codeInsight.intention.LowPriorityAction
+import com.intellij.modcommand.ModPsiUpdater
+import com.intellij.modcommand.PsiUpdateModCommandQuickFix
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElement
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.psi.PyElementGenerator
+import com.jetbrains.python.psi.PyStringLiteralExpression
+
+class PyConvertToRawStringQuickFix(private val offsetInElement: Int) : PsiUpdateModCommandQuickFix(), LowPriorityAction {
+ override fun getFamilyName(): String = PyPsiBundle.message("QFIX.convert.to.raw.string")
+
+ override fun applyFix(project: Project, element: PsiElement, updater: ModPsiUpdater) {
+ if (element !is PyStringLiteralExpression) return
+
+ val part = element.stringElements.find { it.textRangeInParent.contains(offsetInElement) } ?: return
+ if (part.prefix.contains("r", ignoreCase = true)) return
+
+ val newText = "r" + part.text
+
+ val newPart = PyElementGenerator.getInstance(project)
+ .createStringLiteralAlreadyEscaped(newText)
+ .stringElements.firstOrNull() ?: return
+ part.replace(newPart)
+ }
+}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyEscapeBackslashQuickFix.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyEscapeBackslashQuickFix.kt
new file mode 100644
index 000000000000..ebc33be81de3
--- /dev/null
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/quickfix/PyEscapeBackslashQuickFix.kt
@@ -0,0 +1,25 @@
+package com.jetbrains.python.inspections.quickfix
+
+import com.intellij.modcommand.ModPsiUpdater
+import com.intellij.modcommand.PsiUpdateModCommandQuickFix
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElement
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.psi.PyElementGenerator
+import com.jetbrains.python.psi.PyStringLiteralExpression
+
+class PyEscapeBackslashQuickFix(private val offsetInElement: Int) : PsiUpdateModCommandQuickFix() {
+ override fun getFamilyName(): String = PyPsiBundle.message("QFIX.escape.backslash")
+
+ override fun applyFix(project: Project, element: PsiElement, updater: ModPsiUpdater) {
+ if (element !is PyStringLiteralExpression) return
+
+ if (offsetInElement >= element.textLength || element.text[offsetInElement] != '\\') return
+
+ val newText = StringBuilder(element.text).insert(offsetInElement, "\\").toString()
+
+ val newElement = PyElementGenerator.getInstance(project)
+ .createStringLiteralAlreadyEscaped(newText)
+ element.replace(newElement)
+ }
+}
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/basic.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/basic.py
new file mode 100644
index 000000000000..b85d676f8cef
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/basic.py
@@ -0,0 +1,9 @@
+s1 = "\."
+
+s2 = "\z"
+
+s3 = "\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
+
+s4 = r"\."
+
+s5 = " \. \z "
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/bytes.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/bytes.py
new file mode 100644
index 000000000000..e2ab04f7d2ce
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/bytes.py
@@ -0,0 +1,9 @@
+b1 = b"\."
+
+b2 = b"\z"
+
+b3 = b"\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
+
+b4 = rb"\."
+
+b5 = b" \. \z "
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawStringNotAvailable_before.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawStringNotAvailable_before.py
new file mode 100644
index 000000000000..9b72b5d1c3bc
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawStringNotAvailable_before.py
@@ -0,0 +1 @@
+s = "postfix" '\n\.' "suffix"
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_after.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_after.py
new file mode 100644
index 000000000000..0a357fc72cd9
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_after.py
@@ -0,0 +1 @@
+s = "prefix" r'\.' "suffix"
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_before.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_before.py
new file mode 100644
index 000000000000..8cbd9269d7b0
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/convertToRawString_before.py
@@ -0,0 +1 @@
+s = "prefix" '\.' "suffix"
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_after.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_after.py
new file mode 100644
index 000000000000..9a6940d661bc
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_after.py
@@ -0,0 +1 @@
+s = "prefix" '\\.' "suffix"
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_before.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_before.py
new file mode 100644
index 000000000000..8cbd9269d7b0
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/escapeBackslash_before.py
@@ -0,0 +1 @@
+s = "prefix" '\.' "suffix"
diff --git a/python/testData/inspections/PyInvalidEscapeSequenceInspection/fstrings.py b/python/testData/inspections/PyInvalidEscapeSequenceInspection/fstrings.py
new file mode 100644
index 000000000000..c3c162c2d300
--- /dev/null
+++ b/python/testData/inspections/PyInvalidEscapeSequenceInspection/fstrings.py
@@ -0,0 +1,44 @@
+f1 = f"\."
+
+f2 = f"\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
+
+f3 = f"foo {1} \ "
+
+f4 = f"foo { f'\.' }"
+
+f5 = f"level1 { f'level2 { f"level3 \." }' } \."
+
+f6 = f"valid \n invalid \. valid \t"
+
+f7 = f"{{ \. }}"
+
+f8 = f"\{{"
+
+f9 = f"""
+ \.
+ {1}
+ valid \n
+ \z
+"""
+
+f10 = f"text \. { '\n' } text"
+
+f11 = f"list {[1, 2]} set { {1, 2} } dict { {'k': 'v'} } \."
+
+# \n and \x are escape sequences, while \z is not:
+f12 = f"expression { 1 + \
+n } \."
+
+f13 = f"nested { f'inner { 1 + \
+x }' } "
+
+f16 = f"expression { 1 + \
+z } \."
+
+f14 = f" { '\n' } { '\.' } "
+
+f15 = f" { {'key': '\n', 'val': '\.'} } "
+
+rf1 = rf"\."
+rf2 = rf"foo {1} \."
+rf3 = fr"foo {1} \."
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspectionTest.kt
new file mode 100644
index 000000000000..4466c09a72c2
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/inspections/PyInvalidEscapeSequenceInspectionTest.kt
@@ -0,0 +1,43 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.inspections
+
+import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.fixtures.PyInspectionTestCase
+
+class PyInvalidEscapeSequenceInspectionTest : PyInspectionTestCase() {
+
+ override fun getInspectionClass(): Class = PyInvalidEscapeSequenceInspection::class.java
+
+ fun testBasic() = doTest()
+
+ fun testBytes() = doTest()
+
+ fun testFstrings() = doTest()
+
+ fun testEscapeBackslash() {
+ doQuickFixTest(PyPsiBundle.message("QFIX.escape.backslash"), "escapeBackslash_before.py", "escapeBackslash_after.py")
+ }
+
+ fun testConvertToRawString() {
+ doQuickFixTest(PyPsiBundle.message("QFIX.convert.to.raw.string"), "convertToRawString_before.py", "convertToRawString_after.py")
+ }
+
+ fun testConvertToRawStringNotAvailable() {
+ myFixture.configureByFile(testCaseDirectory + "convertToRawStringNotAvailable_before.py")
+ myFixture.enableInspections(getInspectionClass())
+
+ val actions = myFixture.filterAvailableIntentions(PyPsiBundle.message("QFIX.convert.to.raw.string"))
+ assertEmpty("Convert to raw string should not be available if string contains valid escapes", actions)
+
+ assertNotNull(myFixture.findSingleIntention(PyPsiBundle.message("QFIX.escape.backslash")))
+ }
+
+ private fun doQuickFixTest(hint: String, sourceFile: String, expectedFile: String) {
+ myFixture.configureByFile(testCaseDirectory + sourceFile)
+ myFixture.enableInspections(getInspectionClass())
+
+ val action = myFixture.findSingleIntention(hint)
+ myFixture.launchAction(action)
+ myFixture.checkResultByFile(testCaseDirectory + expectedFile)
+ }
+}