Deprecate Python 2 formatting support
diff --git a/CHANGES.md b/CHANGES.md
index 4c04eccde48..9990d8cf459 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,7 @@
 - Bumped typed-ast version minimum to 1.4.3 for 3.10 compatiblity (#2519)
 - Fixed a Python 3.10 compatibility issue where the loop argument was still being passed
   even though it has been removed (#2580)
+- Deprecate Python 2 formatting support (#2523)
 
 ### _Blackd_
 
diff --git a/docs/faq.md b/docs/faq.md
index 9fe53922b6d..77f9df51fd4 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -74,13 +74,17 @@ disabled-by-default counterpart W504. E203 should be disabled while changes are
 
 ## Does Black support Python 2?
 
+```{warning}
+Python 2 support has been deprecated since 21.10b0.
+
+This support will be dropped in the first stable release, expected for January 2022.
+See [The Black Code Style](the_black_code_style/index.rst) for details.
+```
+
 For formatting, yes! [Install](getting_started.md#installation) with the `python2` extra
 to format Python 2 files too! In terms of running _Black_ though, Python 3.6 or newer is
 required.
 
-Note that this support will be dropped in the first stable release, expected for
-January 2022. See [The Black Code Style](the_black_code_style/index.rst) for details.
-
 ## Why does my linter or typechecker complain after I format my code?
 
 Some linters and other tools use magical comments (e.g., `# noqa`, `# type: ignore`) to
diff --git a/src/black/__init__.py b/src/black/__init__.py
index c503c1a55f7..831cda934a8 100644
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -1061,6 +1061,15 @@ def f(
         versions = mode.target_versions
     else:
         versions = detect_target_versions(src_node)
+
+    # TODO: fully drop support and this code hopefully in January 2022 :D
+    if TargetVersion.PY27 in mode.target_versions or versions == {TargetVersion.PY27}:
+        msg = (
+            "DEPRECATION: Python 2 support will be removed in the first stable release"
+            "expected in January 2022."
+        )
+        err(msg, fg="yellow", bold=True)
+
     normalize_fmt_off(src_node)
     lines = LineGenerator(
         mode=mode,
@@ -1103,7 +1112,7 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
         return tiow.read(), encoding, newline
 
 
-def get_features_used(node: Node) -> Set[Feature]:
+def get_features_used(node: Node) -> Set[Feature]:  # noqa: C901
     """Return a set of (relatively) new Python features used in this file.
 
     Currently looking for:
@@ -1113,6 +1122,7 @@ def get_features_used(node: Node) -> Set[Feature]:
     - positional only arguments in function signatures and lambdas;
     - assignment expression;
     - relaxed decorator syntax;
+    - print / exec statements;
     """
     features: Set[Feature] = set()
     for n in node.pre_order():
@@ -1161,6 +1171,11 @@ def get_features_used(node: Node) -> Set[Feature]:
                         if argch.type in STARS:
                             features.add(feature)
 
+        elif n.type == token.PRINT_STMT:
+            features.add(Feature.PRINT_STMT)
+        elif n.type == token.EXEC_STMT:
+            features.add(Feature.EXEC_STMT)
+
     return features
 
 
diff --git a/src/black/mode.py b/src/black/mode.py
index 0b7624eaf8a..374c47a42eb 100644
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -41,9 +41,17 @@ class Feature(Enum):
     RELAXED_DECORATORS = 10
     FORCE_OPTIONAL_PARENTHESES = 50
 
+    # temporary for Python 2 deprecation
+    PRINT_STMT = 200
+    EXEC_STMT = 201
+
 
 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
-    TargetVersion.PY27: {Feature.ASYNC_IDENTIFIERS},
+    TargetVersion.PY27: {
+        Feature.ASYNC_IDENTIFIERS,
+        Feature.PRINT_STMT,
+        Feature.EXEC_STMT,
+    },
     TargetVersion.PY33: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
     TargetVersion.PY34: {Feature.UNICODE_LITERALS, Feature.ASYNC_IDENTIFIERS},
     TargetVersion.PY35: {
diff --git a/src/blib2to3/pgen2/token.py b/src/blib2to3/pgen2/token.py
index 1e0dec9c714..349ba8023a2 100644
--- a/src/blib2to3/pgen2/token.py
+++ b/src/blib2to3/pgen2/token.py
@@ -74,6 +74,9 @@
 COLONEQUAL: Final = 59
 N_TOKENS: Final = 60
 NT_OFFSET: Final = 256
+# temporary for Python 2 deprecation
+PRINT_STMT: Final = 316
+EXEC_STMT: Final = 288
 # --end constants--
 
 tok_name: Final[Dict[int, str]] = {}
diff --git a/tests/test_black.py b/tests/test_black.py
index 5647a00e48b..b96a5438557 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -1401,14 +1401,14 @@ def test_docstring_reformat_for_py27(self) -> None:
         )
         expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
 
-        result = CliRunner().invoke(
+        result = BlackRunner().invoke(
             black.main,
             ["-", "-q", "--target-version=py27"],
             input=BytesIO(source),
         )
 
         self.assertEqual(result.exit_code, 0)
-        actual = result.output
+        actual = result.stdout
         self.assertFormatEqual(actual, expected)
 
     @staticmethod
@@ -2017,6 +2017,21 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
         )
 
 
+@pytest.mark.parametrize("explicit", [True, False], ids=["explicit", "autodetection"])
+def test_python_2_deprecation_with_target_version(explicit: bool) -> None:
+    args = [
+        "--config",
+        str(THIS_DIR / "empty.toml"),
+        str(DATA_DIR / "python2.py"),
+        "--check",
+    ]
+    if explicit:
+        args.append("--target-version=py27")
+    with cache_dir():
+        result = BlackRunner().invoke(black.main, args)
+    assert "DEPRECATION: Python 2 support will be removed" in result.stderr
+
+
 with open(black.__file__, "r", encoding="utf-8") as _bf:
     black_source_lines = _bf.readlines()
 

### Description

It's 2021. See also #2251.

### Checklist - did you ...

<!-- If any of the following items aren't relevant for your contribution
     please still tick them so we know you've gone through the checklist.

    All user-facing changes should get an entry. Otherwise, signal to us
    this should get the magical label to silence the CHANGELOG entry check.
    Tests are required for bugfixes and new features. Documentation changes
    are necessary for formatting and most enhancement changes. -->

- [x] Add a CHANGELOG entry if necessary?
- [x] Add / update tests if necessary?
- [x] Add new / update outdated documentation?