Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid `try` with `finally`without `except`
# Rule request

## Thesis

We should forbid using `try` block with anything other than `except`.

## Reasoning

Related: https://help.semmle.com/wiki/display/PYTHON/Should+use+a+%27with%27+statement

Let's remember why do we use `try` with `finally`. To clean thing up when something explodes inside `try` block. That a popular pattern for files, streams, etc.

What else can be used in this case? We can use `with` to make this code readable. Consider this example:

```python
try:
    f = open("filename")
    f.write(...)
finally:
    f.close()
```

Versus context manager:

```python
with open("filename") as f:
    f.write(...)
```

The second version looks cleaner and more readable. 

## Limitations

This rule is limited to cases when there are no `except` blocks. Because when there are except blocks `finally` becomes very useful to execute any logic after both successful and exceptional branches.


Discussion:
@novikovfred do you have the time to take this one? Should be a really simple one.
@sobolevn ye, i'll do it

Resolving patch:
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -37,6 +37,7 @@
    BadMagicMethodViolation
    NestedImportViolation
    RedundantForElseViolation
+   RedundantFinallyViolation
 
 Comments
 --------
@@ -68,6 +69,7 @@
 .. autoclass:: BadMagicMethodViolation
 .. autoclass:: NestedImportViolation
 .. autoclass:: RedundantForElseViolation
+.. autoclass:: RedundantFinallyViolation
 
 """
 
@@ -615,3 +617,37 @@ class RedundantForElseViolation(ASTViolation):
     #: Error message shown to the user.
     error_template = 'Found `else` in `for` loop with `break`'
     code = 436
+
+
+class RedundantFinallyViolation(ASTViolation):
+    """
+    Forbids to use ``finally`` in ``try`` block without ``except`` block.
+
+    Reasoning:
+        This rule will reduce complexity and improve readability.
+
+    Solution:
+        Refactor your ``try`` logic.
+        Replace the ``try-finally`` statement with a ``with`` statement.
+
+    Example::
+
+        # Correct:
+        with open("filename") as f:
+            f.write(...)
+
+        # Wrong:
+        try:
+            f = open("filename")
+            f.write(...)
+        finally:
+            f.close()
+
+    Note:
+        Returns Z437 as error code
+
+    """
+
+    #: Error message shown to the user.
+    error_template = 'Found `finally` in `try` block without `except`'
+    code = 437
diff --git a/wemake_python_styleguide/visitors/ast/keywords.py b/wemake_python_styleguide/visitors/ast/keywords.py
--- a/wemake_python_styleguide/visitors/ast/keywords.py
+++ b/wemake_python_styleguide/visitors/ast/keywords.py
@@ -6,6 +6,7 @@
 from wemake_python_styleguide.types import AnyNodes
 from wemake_python_styleguide.violations.best_practices import (
     RaiseNotImplementedViolation,
+    RedundantFinallyViolation,
     RedundantForElseViolation,
     WrongKeywordViolation,
 )
@@ -108,3 +109,16 @@ def visit_For(self, node: ast.For) -> None:
         """Used for find else block in for loops with break."""
         self._check_for_needs_else(node)
         self.generic_visit(node)
+
+
+class WrongTryFinallyVisitor(BaseNodeVisitor):
+    """Responsible for restricting finally in try blocks without except."""
+
+    def _check_for_needs_except(self, node: ast.Try) -> None:
+        if node.finalbody and not node.handlers:
+            self.add_violation(RedundantFinallyViolation(node=node))
+
+    def visit_Try(self, node: ast.Try) -> None:
+        """Used for find finally in try blocks without except."""
+        self._check_for_needs_except(node)
+        self.generic_visit(node)
diff --git a/wemake_python_styleguide/visitors/presets/general.py b/wemake_python_styleguide/visitors/presets/general.py
--- a/wemake_python_styleguide/visitors/presets/general.py
+++ b/wemake_python_styleguide/visitors/presets/general.py
@@ -26,6 +26,7 @@
     keywords.WrongKeywordVisitor,
     keywords.WrongListComprehensionVisitor,
     keywords.WrongForElseVisitor,
+    keywords.WrongTryFinallyVisitor,
 
     WrongFunctionCallVisitor,
     WrongImportVisitor,


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -66,3 +66,9 @@ class Nested:  # noqa: Z306,Z431
     break
 else:
     ...
+
+
+try:  # noqa: Z437
+    ...
+finally:
+    ...
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -52,6 +52,7 @@ def test_noqa_fixture_disabled(absolute_path):
         'Z434': 1,
         'Z435': 1,
         'Z436': 1,
+        'Z437': 1,
     }
 
     process = subprocess.Popen(
diff --git a/tests/test_visitors/test_ast/test_keywords/test_try.py b/tests/test_visitors/test_ast/test_keywords/test_try.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_keywords/test_try.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    RedundantFinallyViolation,
+)
+from wemake_python_styleguide.visitors.ast.keywords import (
+    WrongTryFinallyVisitor,
+)
+
+right_try_example = """
+try:
+    ...
+except:
+    ...
+"""
+
+wrong_try_example = """
+try:
+    ...
+finally:
+    ...
+"""
+
+check_finally_with_except = """
+try:
+    ...
+except:
+    ...
+finally:
+    ...
+"""
+
+
+@pytest.mark.parametrize('code', [
+    wrong_try_example,
+])
+def test_wrong_finally(
+    assert_errors, parse_ast_tree, code, default_options,
+):
+    """Violations are raised when finally without except in try block."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongTryFinallyVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [RedundantFinallyViolation])
+
+
+@pytest.mark.parametrize('code', [
+    right_try_example,
+    check_finally_with_except,
+])
+def test_correct_finally(
+    assert_errors, parse_ast_tree, code, default_options,
+):
+    """Violations are not raised when finally with except in try block."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongTryFinallyVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])