diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 26c9884e1813..9f42e2738cfc 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -41,6 +41,8 @@ def __init__(
         namespaces=None,
         route=None,
         tried=None,
+        captured_kwargs=None,
+        extra_kwargs=None,
     ):
         self.func = func
         self.args = args
@@ -48,6 +50,8 @@ def __init__(
         self.url_name = url_name
         self.route = route
         self.tried = tried
+        self.captured_kwargs = captured_kwargs
+        self.extra_kwargs = extra_kwargs

         # If a URLRegexResolver doesn't have a namespace or app_name, it passes
         # in an empty value.
@@ -78,7 +82,7 @@ def __repr__(self):
             func = self._func_path
         return (
             "ResolverMatch(func=%s, args=%r, kwargs=%r, url_name=%r, "
-            "app_names=%r, namespaces=%r, route=%r)"
+            "app_names=%r, namespaces=%r, route=%r%s%s)"
             % (
                 func,
                 self.args,
@@ -87,6 +91,10 @@ def __repr__(self):
                 self.app_names,
                 self.namespaces,
                 self.route,
+                f", captured_kwargs={self.captured_kwargs!r}"
+                if self.captured_kwargs
+                else "",
+                f", extra_kwargs={self.extra_kwargs!r}" if self.extra_kwargs else "",
             )
         )

@@ -416,11 +424,17 @@ def _check_callback(self):
     def resolve(self, path):
         match = self.pattern.match(path)
         if match:
-            new_path, args, kwargs = match
-            # Pass any extra_kwargs as **kwargs.
-            kwargs.update(self.default_args)
+            new_path, args, captured_kwargs = match
+            # Pass any default args as **kwargs.
+            kwargs = {**captured_kwargs, **self.default_args}
             return ResolverMatch(
-                self.callback, args, kwargs, self.pattern.name, route=str(self.pattern)
+                self.callback,
+                args,
+                kwargs,
+                self.pattern.name,
+                route=str(self.pattern),
+                captured_kwargs=captured_kwargs,
+                extra_kwargs=self.default_args,
             )

     @cached_property
@@ -678,6 +692,11 @@ def resolve(self, path):
                             [self.namespace] + sub_match.namespaces,
                             self._join_route(current_route, sub_match.route),
                             tried,
+                            captured_kwargs=sub_match.captured_kwargs,
+                            extra_kwargs={
+                                **self.default_kwargs,
+                                **sub_match.extra_kwargs,
+                            },
                         )
                     tried.append([pattern])
             raise Resolver404({"tried": tried, "path": new_path})
@@ -737,7 +756,14 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
                 else:
                     if set(kwargs).symmetric_difference(params).difference(defaults):
                         continue
-                    if any(kwargs.get(k, v) != v for k, v in defaults.items()):
+                    matches = True
+                    for k, v in defaults.items():
+                        if k in params:
+                            continue
+                        if kwargs.get(k, v) != v:
+                            matches = False
+                            break
+                    if not matches:
                         continue
                     candidate_subs = kwargs
                 # Convert the candidate subs to text using Converter.to_url().
diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt
index d39345b478fc..2bde4d73eaf7 100644
--- a/docs/ref/urlresolvers.txt
+++ b/docs/ref/urlresolvers.txt
@@ -123,9 +123,24 @@ If the URL does not resolve, the function raises a

     .. attribute:: ResolverMatch.kwargs

-        The keyword arguments that would be passed to the view
+        All keyword arguments that would be passed to the view function, i.e.
+        :attr:`~ResolverMatch.captured_kwargs` and
+        :attr:`~ResolverMatch.extra_kwargs`.
+
+    .. attribute:: ResolverMatch.captured_kwargs
+
+        .. versionadded:: 4.1
+
+        The captured keyword arguments that would be passed to the view
         function, as parsed from the URL.

+    .. attribute:: ResolverMatch.extra_kwargs
+
+        .. versionadded:: 4.1
+
+        The additional keyword arguments that would be passed to the view
+        function.
+
     .. attribute:: ResolverMatch.url_name

         The name of the URL pattern that matches the URL.
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index c7159bd4f237..eb4bde652ba5 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -326,7 +326,11 @@ Tests
 URLs
 ~~~~

-* ...
+* The new :attr:`.ResolverMatch.captured_kwargs` attribute stores the captured
+  keyword arguments, as parsed from the URL.
+
+* The new :attr:`.ResolverMatch.extra_kwargs` attribute stores the additional
+  keyword arguments passed to the view function.

 Utilities
 ~~~~~~~~~

diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py
index db04e9a1a495..43d95ff668fd 100644
--- a/tests/i18n/patterns/tests.py
+++ b/tests/i18n/patterns/tests.py
@@ -8,7 +8,7 @@
 from django.test import SimpleTestCase, override_settings
 from django.test.client import RequestFactory
 from django.test.utils import override_script_prefix
-from django.urls import clear_url_caches, reverse, translate_url
+from django.urls import clear_url_caches, resolve, reverse, translate_url
 from django.utils import translation

@@ -198,6 +198,23 @@ def test_translate_url_utility(self):
             self.assertEqual(translate_url("/nl/gebruikers/", "en"), "/en/users/")
             self.assertEqual(translation.get_language(), "nl")

+    def test_reverse_translated_with_captured_kwargs(self):
+        with translation.override("en"):
+            match = resolve("/translated/apo/")
+        # Links to the same page in other languages.
+        tests = [
+            ("nl", "/vertaald/apo/"),
+            ("pt-br", "/traduzidos/apo/"),
+        ]
+        for lang, expected_link in tests:
+            with translation.override(lang):
+                self.assertEqual(
+                    reverse(
+                        match.url_name, args=match.args, kwargs=match.captured_kwargs
+                    ),
+                    expected_link,
+                )
+

 class URLNamespaceTests(URLTestCaseBase):
     """
diff --git a/tests/i18n/patterns/urls/default.py b/tests/i18n/patterns/urls/default.py
index c77bf98c7390..418e9f568568 100644
--- a/tests/i18n/patterns/urls/default.py
+++ b/tests/i18n/patterns/urls/default.py
@@ -10,7 +10,10 @@
     path("not-prefixed-include/", include("i18n.patterns.urls.included")),
     re_path(_(r"^translated/$"), view, name="no-prefix-translated"),
     re_path(
-        _(r"^translated/(?P<slug>[\w-]+)/$"), view, name="no-prefix-translated-slug"
+        _(r"^translated/(?P<slug>[\w-]+)/$"),
+        view,
+        {"slug": "default-slug"},
+        name="no-prefix-translated-slug",
     ),
 ]

diff --git a/tests/urlpatterns/more_urls.py b/tests/urlpatterns/more_urls.py
index bbfd962c4e58..8fc3abd51867 100644
--- a/tests/urlpatterns/more_urls.py
+++ b/tests/urlpatterns/more_urls.py
@@ -3,5 +3,10 @@
 from . import views

 urlpatterns = [
-    re_path(r"^more/(?P<extra>\w+)/$", views.empty_view, name="inner-more"),
+    re_path(
+        r"^more/(?P<extra>\w+)/$",
+        views.empty_view,
+        {"sub-extra": True},
+        name="inner-more",
+    ),
 ]
diff --git a/tests/urlpatterns/path_urls.py b/tests/urlpatterns/path_urls.py
index 5ebdfe1b168e..8a356b546341 100644
--- a/tests/urlpatterns/path_urls.py
+++ b/tests/urlpatterns/path_urls.py
@@ -13,6 +13,13 @@
         views.empty_view,
         name="articles-year-month-day",
     ),
+    path("books/2007/", views.empty_view, {"extra": True}, name="books-2007"),
+    path(
+        "books/<int:year>/<int:month>/<int:day>/",
+        views.empty_view,
+        {"extra": True},
+        name="books-year-month-day",
+    ),
     path("users/", views.empty_view, name="users"),
     path("users/<id>/", views.empty_view, name="user-with-id"),
     path("included_urls/", include("urlpatterns.included_urls")),
@@ -27,6 +34,6 @@
         views.empty_view,
         name="regex_only_optional",
     ),
-    path("", include("urlpatterns.more_urls")),
+    path("", include("urlpatterns.more_urls"), {"sub-extra": False}),
     path("<lang>/<path:url>/", views.empty_view, name="lang-and-path"),
 ]
diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py
index cbe61278dae6..f8d73fdb4ad3 100644
--- a/tests/urlpatterns/tests.py
+++ b/tests/urlpatterns/tests.py
@@ -34,6 +34,8 @@ def test_path_lookup_without_parameters(self):
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {})
         self.assertEqual(match.route, "articles/2003/")
+        self.assertEqual(match.captured_kwargs, {})
+        self.assertEqual(match.extra_kwargs, {})

     def test_path_lookup_with_typed_parameters(self):
         match = resolve("/articles/2015/")
@@ -41,6 +43,8 @@ def test_path_lookup_with_typed_parameters(self):
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {"year": 2015})
         self.assertEqual(match.route, "articles/<int:year>/")
+        self.assertEqual(match.captured_kwargs, {"year": 2015})
+        self.assertEqual(match.extra_kwargs, {})

     def test_path_lookup_with_multiple_parameters(self):
         match = resolve("/articles/2015/04/12/")
@@ -48,18 +52,44 @@ def test_path_lookup_with_multiple_parameters(self):
         self.assertEqual(match.args, ())
         self.assertEqual(match.kwargs, {"year": 2015, "month": 4, "day": 12})
         self.assertEqual(match.route, "articles/<int:year>/<int:month>/<int:day>/")
+        self.assertEqual(match.captured_kwargs, {"year": 2015, "month": 4, "day": 12})
+        self.assertEqual(match.extra_kwargs, {})
+
+    def test_path_lookup_with_multiple_parameters_and_extra_kwarg(self):
+        match = resolve("/books/2015/04/12/")
+        self.assertEqual(match.url_name, "books-year-month-day")
+        self.assertEqual(match.args, ())
+        self.assertEqual(
+            match.kwargs, {"year": 2015, "month": 4, "day": 12, "extra": True}
+        )
+        self.assertEqual(match.route, "books/<int:year>/<int:month>/<int:day>/")
+        self.assertEqual(match.captured_kwargs, {"year": 2015, "month": 4, "day": 12})
+        self.assertEqual(match.extra_kwargs, {"extra": True})
+
+    def test_path_lookup_with_extra_kwarg(self):
+        match = resolve("/books/2007/")
+        self.assertEqual(match.url_name, "books-2007")
+        self.assertEqual(match.args, ())
+        self.assertEqual(match.kwargs, {"extra": True})
+        self.assertEqual(match.route, "books/2007/")
+        self.assertEqual(match.captured_kwargs, {})
+        self.assertEqual(match.extra_kwargs, {"extra": True})

     def test_two_variable_at_start_of_path_pattern(self):
         match = resolve("/en/foo/")
         self.assertEqual(match.url_name, "lang-and-path")
         self.assertEqual(match.kwargs, {"lang": "en", "url": "foo"})
         self.assertEqual(match.route, "<lang>/<path:url>/")
+        self.assertEqual(match.captured_kwargs, {"lang": "en", "url": "foo"})
+        self.assertEqual(match.extra_kwargs, {})

     def test_re_path(self):
         match = resolve("/regex/1/")
         self.assertEqual(match.url_name, "regex")
         self.assertEqual(match.kwargs, {"pk": "1"})
         self.assertEqual(match.route, "^regex/(?P<pk>[0-9]+)/$")
+        self.assertEqual(match.captured_kwargs, {"pk": "1"})
+        self.assertEqual(match.extra_kwargs, {})

     def test_re_path_with_optional_parameter(self):
         for url, kwargs in (
@@ -74,6 +104,8 @@ def test_re_path_with_optional_parameter(self):
                     match.route,
                     r"^regex_optional/(?P<arg1>\d+)/(?:(?P<arg2>\d+)/)?",
                 )
+                self.assertEqual(match.captured_kwargs, kwargs)
+                self.assertEqual(match.extra_kwargs, {})

     def test_re_path_with_missing_optional_parameter(self):
         match = resolve("/regex_only_optional/")
@@ -84,6 +116,8 @@ def test_re_path_with_missing_optional_parameter(self):
             match.route,
             r"^regex_only_optional/(?:(?P<arg1>\d+)/)?",
         )
+        self.assertEqual(match.captured_kwargs, {})
+        self.assertEqual(match.extra_kwargs, {})

     def test_path_lookup_with_inclusion(self):
         match = resolve("/included_urls/extra/something/")
@@ -94,6 +128,9 @@ def test_path_lookup_with_empty_string_inclusion(self):
         match = resolve("/more/99/")
         self.assertEqual(match.url_name, "inner-more")
         self.assertEqual(match.route, r"^more/(?P<extra>\w+)/$")
+        self.assertEqual(match.kwargs, {"extra": "99", "sub-extra": True})
+        self.assertEqual(match.captured_kwargs, {"extra": "99"})
+        self.assertEqual(match.extra_kwargs, {"sub-extra": True})

     def test_path_lookup_with_double_inclusion(self):
         match = resolve("/included_urls/more/some_value/")
diff --git a/tests/urlpatterns_reverse/namespace_urls.py b/tests/urlpatterns_reverse/namespace_urls.py
index 2e3db3ecc3c3..4a39ab229015 100644
--- a/tests/urlpatterns_reverse/namespace_urls.py
+++ b/tests/urlpatterns_reverse/namespace_urls.py
@@ -23,7 +23,10 @@
     path("resolver_match/", views.pass_resolver_match_view, name="test-resolver-match"),
     re_path(r"^\+\\\$\*/$", views.empty_view, name="special-view"),
     re_path(
-        r"^mixed_args/([0-9]+)/(?P<arg2>[0-9]+)/$", views.empty_view, name="mixed-args"
+        r"^mixed_args/([0-9]+)/(?P<arg2>[0-9]+)/$",
+        views.empty_view,
+        {"extra": True},
+        name="mixed-args",
     ),
     re_path(r"^no_kwargs/([0-9]+)/([0-9]+)/$", views.empty_view, name="no-kwargs"),
     re_path(
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index 59c22dd589c8..89dfd0deba4f 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -89,7 +89,7 @@
         "mixed-args",
         views.empty_view,
         (),
-        {"arg2": "37"},
+        {"extra": True, "arg2": "37"},
     ),
     (
         "/included/mixed_args/42/37/",
@@ -1554,6 +1554,16 @@ def test_repr(self):
             "namespaces=[], route='^no_kwargs/([0-9]+)/([0-9]+)/$')",
         )

+    def test_repr_extra_kwargs(self):
+        self.assertEqual(
+            repr(resolve("/mixed_args/1986/11/")),
+            "ResolverMatch(func=urlpatterns_reverse.views.empty_view, args=(), "
+            "kwargs={'arg2': '11', 'extra': True}, url_name='mixed-args', "
+            "app_names=[], namespaces=[], "
+            "route='^mixed_args/([0-9]+)/(?P<arg2>[0-9]+)/$', "
+            "captured_kwargs={'arg2': '11'}, extra_kwargs={'extra': True})",
+        )
+
     @override_settings(ROOT_URLCONF="urlpatterns_reverse.reverse_lazy_urls")
     def test_classbased_repr(self):
         self.assertEqual(
@@ -1758,3 +1768,18 @@ def test_invalid_reverse(self):
             with self.subTest(name=name, kwargs=kwargs):
                 with self.assertRaises(NoReverseMatch):
                     reverse(name, kwargs=kwargs)
+
+
+@override_settings(ROOT_URLCONF="urlpatterns_reverse.urls")
+class ReverseResolvedTests(SimpleTestCase):
+    def test_rereverse(self):
+        match = resolve("/resolved/12/")
+        self.assertEqual(
+            reverse(match.url_name, args=match.args, kwargs=match.kwargs),
+            "/resolved/12/",
+        )
+        match = resolve("/resolved-overridden/12/url/")
+        self.assertEqual(
+            reverse(match.url_name, args=match.args, kwargs=match.captured_kwargs),
+            "/resolved-overridden/12/url/",
+        )
diff --git a/tests/urlpatterns_reverse/urls.py b/tests/urlpatterns_reverse/urls.py
index ba3f51b563ce..c7453314835a 100644
--- a/tests/urlpatterns_reverse/urls.py
+++ b/tests/urlpatterns_reverse/urls.py
@@ -78,6 +78,13 @@
         name="windows",
     ),
     re_path(r"^special_chars/(?P<chars>.+)/$", empty_view, name="special"),
+    re_path(r"^resolved/(?P<arg>\d+)/$", empty_view, {"extra": True}, name="resolved"),
+    re_path(
+        r"^resolved-overridden/(?P<arg>\d+)/(?P<overridden>\w+)/$",
+        empty_view,
+        {"extra": True, "overridden": "default"},
+        name="resolved-overridden",
+    ),
     re_path(r"^(?P<name>.+)/[0-9]+/$", empty_view, name="mixed"),
     re_path(r"^repeats/a{1,2}/$", empty_view, name="repeats"),
     re_path(r"^repeats/a{2,4}/$", empty_view, name="repeats2"),

diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt
index d39345b478..2bde4d73ea 100644
--- a/docs/ref/urlresolvers.txt
+++ b/docs/ref/urlresolvers.txt
@@ -123,9 +123,24 @@ If the URL does not resolve, the function raises a

     .. attribute:: ResolverMatch.kwargs

-        The keyword arguments that would be passed to the view
+        All keyword arguments that would be passed to the view function, i.e.
+        :attr:`~ResolverMatch.captured_kwargs` and
+        :attr:`~ResolverMatch.extra_kwargs`.
+
+    .. attribute:: ResolverMatch.captured_kwargs
+
+        .. versionadded:: 4.1
+
+        The captured keyword arguments that would be passed to the view
         function, as parsed from the URL.

+    .. attribute:: ResolverMatch.extra_kwargs
+
+        .. versionadded:: 4.1
+
+        The additional keyword arguments that would be passed to the view
+        function.
+
     .. attribute:: ResolverMatch.url_name

         The name of the URL pattern that matches the URL.

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index c7159bd4f2..eb4bde652b 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -326,7 +326,11 @@ Tests
 URLs
 ~~~~

-* ...
+* The new :attr:`.ResolverMatch.captured_kwargs` attribute stores the captured
+  keyword arguments, as parsed from the URL.
+
+* The new :attr:`.ResolverMatch.extra_kwargs` attribute stores the additional
+  keyword arguments passed to the view function.

 Utilities
 ~~~~~~~~~


diff --git a/django/forms/forms.py b/django/forms/forms.py
index 952b97413072..2500dccc9bcf 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -66,7 +66,6 @@ class BaseForm(RenderableFormMixin):
     prefix = None
     use_required_attribute = True

-    template_name = "django/forms/default.html"
     template_name_p = "django/forms/p.html"
     template_name_table = "django/forms/table.html"
     template_name_ul = "django/forms/ul.html"
@@ -316,6 +315,10 @@ def _html_output(
                 output.append(str_hidden)
         return mark_safe("\n".join(output))

+    @property
+    def template_name(self):
+        return self.renderer.form_template_name
+
     def get_context(self):
         fields = []
         hidden_fields = []
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index e5807e8688d1..d51b13548ef0 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -62,7 +62,7 @@ class BaseFormSet(RenderableFormMixin):
             "%(field_names)s. You may need to file a bug report if the issue persists."
         ),
     }
-    template_name = "django/forms/formsets/default.html"
+
     template_name_p = "django/forms/formsets/p.html"
     template_name_table = "django/forms/formsets/table.html"
     template_name_ul = "django/forms/formsets/ul.html"
@@ -517,6 +517,10 @@ def media(self):
         else:
             return self.empty_form.media

+    @property
+    def template_name(self):
+        return self.renderer.formset_template_name
+
     def get_context(self):
         return {"formset": self}

diff --git a/django/forms/renderers.py b/django/forms/renderers.py
index 88cf50465372..0e406c9c7e9f 100644
--- a/django/forms/renderers.py
+++ b/django/forms/renderers.py
@@ -15,6 +15,9 @@ def get_default_renderer():

 class BaseRenderer:
+    form_template_name = "django/forms/default.html"
+    formset_template_name = "django/forms/formsets/default.html"
+
     def get_template(self, template_name):
         raise NotImplementedError("subclasses must implement get_template()")

diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 4d3cf8997d75..a6b4d11f4ad7 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -527,12 +527,18 @@ a form object, and each rendering method returns a string.

 .. attribute:: Form.template_name

-The name of a template that is going to be rendered if the form is cast into a
-string, e.g. via ``print(form)`` or in a template via ``{{ form }}``. By
-default this template is ``'django/forms/default.html'``, which is a proxy for
-``'django/forms/table.html'``. The template can be changed per form by
-overriding the ``template_name`` attribute or more generally by overriding the
-default template, see also :ref:`overriding-built-in-form-templates`.
+The name of the template rendered if the form is cast into a string, e.g. via
+``print(form)`` or in a template via ``{{ form }}``.
+
+By default, a property returning the value of the renderer's
+:attr:`~django.forms.renderers.BaseRenderer.form_template_name`. You may set it
+as a string template name in order to override that for a particular form
+class.
+
+.. versionchanged:: 4.1
+
+    In older versions ``template_name`` defaulted to the string value
+    ``'django/forms/default.html'``.

 ``template_name_label``
 -----------------------
diff --git a/docs/ref/forms/renderers.txt b/docs/ref/forms/renderers.txt
index 9047e2343fb3..8c0263e05186 100644
--- a/docs/ref/forms/renderers.txt
+++ b/docs/ref/forms/renderers.txt
@@ -26,9 +26,16 @@ A custom renderer can be specified by updating the :setting:`FORM_RENDERER`
 setting. It defaults to
 ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``.

-You can also provide a custom renderer by setting the
+By specifying a custom form renderer and overriding
+:attr:`~.BaseRenderer.form_template_name` you can adjust the default form
+markup across your project from a single place.
+
+You can also provide a custom renderer per-form or per-widget by setting the
 :attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
-of :meth:`.Widget.render`.
+of :meth:`.Form.render`, or :meth:`.Widget.render`.
+
+Matching points apply to formset rendering. See :ref:`formset-rendering` for
+discussion.

 Use one of the :ref:`built-in template form renderers
 <built-in-template-form-renderers>` or implement your own. Custom renderers
@@ -36,6 +43,38 @@ must implement a ``render(template_name, context, request=None)`` method. It
 should return a rendered templates (as a string) or raise
 :exc:`~django.template.TemplateDoesNotExist`.

+.. class:: BaseRenderer
+
+    The base class for the built-in form renderers.
+
+    .. attribute:: form_template_name
+
+        .. versionadded:: 4.1
+
+        The default name of the template to use to render a form.
+
+        Defaults to ``"django/forms/default.html"``, which is a proxy for
+        ``"django/forms/table.html"``.
+
+    .. attribute:: formset_template_name
+
+        .. versionadded:: 4.1
+
+        The default name of the template to use to render a formset.
+
+        Defaults to ``"django/forms/formsets/default.html"``, which is a proxy
+        for ``"django/forms/formsets/table.html"``.
+
+    .. method:: get_template(template_name)
+
+        Subclasses must implement this method with the appropriate template
+        finding logic.
+
+    .. method:: render(template_name, context, request=None)
+
+        Renders the given template, or raises
+        :exc:`~django.template.TemplateDoesNotExist`.
+
 .. _built-in-template-form-renderers:

 Built-in-template form renderers
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index b564a325a9b2..4cad556dc387 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -244,6 +244,20 @@ File Uploads
 Forms
 ~~~~~

+* The default template used to render forms when cast to a string, e.g. in
+  templates as ``{{ form }}``, is now configurable at the project-level by
+  setting :attr:`~django.forms.renderers.BaseRenderer.form_template_name` on
+  the class provided for :setting:`FORM_RENDERER`.
+
+  :attr:`.Form.template_name` is now a property deferring to the renderer, but
+  may be overridden with a string value to specify the template name per-form
+  class.
+
+  Similarly, the default template used to render formsets can be specified via
+  the matching
+  :attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
+  attribute.
+
 * The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field
   labels in ``<legend>`` tags via the new ``tag`` argument of
   :meth:`~django.forms.BoundField.label_tag`.
diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt
index a549a047cd0e..0206959f1d1f 100644
--- a/docs/topics/forms/formsets.txt
+++ b/docs/topics/forms/formsets.txt
@@ -783,11 +783,22 @@ Formsets have the following attributes and methods associated with rendering:

     .. versionadded:: 4.0

-    The name of the template used when calling ``__str__`` or :meth:`.render`.
-    This template renders the formset's management form and then each form in
-    the formset as per the template defined by the form's
-    :attr:`~django.forms.Form.template_name`. This is a proxy of ``as_table``
-    by default.
+    The name of the template rendered if the formset is cast into a string,
+    e.g. via ``print(formset)`` or in a template via ``{{ formset }}``.
+
+    By default, a property returning the value of the renderer's
+    :attr:`~django.forms.renderers.BaseRenderer.formset_template_name`. You may
+    set it as a string template name in order to override that for a particular
+    formset class.
+
+    This template will be used to render the formset's management form, and
+    then each form in the formset as per the template defined by the form's
+    :attr:`~django.forms.Form.template_name`.
+
+    .. versionchanged:: 4.1
+
+        In older versions ``template_name`` defaulted to the string value
+        ``'django/forms/formset/default.html'``.

 .. attribute:: BaseFormSet.template_name_p

diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index 2d0d61aa0296..0e98e50692e1 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -759,10 +759,14 @@ Reusable form templates

 If your site uses the same rendering logic for forms in multiple places, you
 can reduce duplication by saving the form's loop in a standalone template and
-overriding the forms :attr:`~django.forms.Form.template_name` attribute to
-render the form using the custom template. The below example will result in
-``{{ form }}`` being rendered as the output of the ``form_snippet.html``
-template.
+setting a custom :setting:`FORM_RENDERER` to use that
+:attr:`~django.forms.renderers.BaseRenderer.form_template_name` site-wide. You
+can also customize per-form by overriding the form's
+:attr:`~django.forms.Form.template_name` attribute to render the form using the
+custom template.
+
+The below example will result in ``{{ form }}`` being rendered as the output of
+the ``form_snippet.html`` template.

 In your templates:

@@ -779,16 +783,42 @@ In your templates:
         </div>
     {% endfor %}

-In your form::
+Then you can configure the :setting:`FORM_RENDERER` setting:
+
+.. code-block:: python
+    :caption: settings.py
+
+    from django.forms.renderers import TemplatesSetting
+
+    class CustomFormRenderer(TemplatesSetting):
+        form_template_name = "form_snippet.html"
+
+    FORM_RENDERER = "project.settings.CustomFormRenderer"
+
+… or for a single form::

     class MyForm(forms.Form):
-        template_name = 'form_snippet.html'
+        template_name = "form_snippet.html"
         ...

+… or for a single render of a form instance, passing in the template name to
+the :meth:`.Form.render()`. Here's an example of this being used in a view::
+
+    def index(request):
+        form = MyForm()
+        rendered_form = form.render("form_snippet.html")
+        context = {'form': rendered_form}
+        return render(request, 'index.html', context)
+
 .. versionchanged:: 4.0

     Template rendering of forms was added.

+.. versionchanged:: 4.1
+
+    The ability to set the default ``form_template_name`` on the form renderer
+    was added.
+
 Further topics
 ==============

diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index c5376b115fa7..ad8f7b9a3186 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -4397,7 +4397,7 @@ class Jinja2FormsTestCase(FormsTestCase):

 class CustomRenderer(DjangoTemplates):
-    pass
+    form_template_name = "forms_tests/form_snippet.html"

 class RendererTests(SimpleTestCase):
@@ -4813,7 +4813,22 @@ def my_function(method, post_data):

 class OverrideTests(SimpleTestCase):
-    def test_use_custom_template(self):
+    @override_settings(FORM_RENDERER="forms_tests.tests.test_forms.CustomRenderer")
+    def test_custom_renderer_template_name(self):
+        class Person(Form):
+            first_name = CharField()
+
+        get_default_renderer.cache_clear()
+        t = Template("{{ form }}")
+        html = t.render(Context({"form": Person()}))
+        expected = """
+
First name:
+        <input type="text" name="first_name" required id="id_first_name">

+        """
+        self.assertHTMLEqual(html, expected)
+        get_default_renderer.cache_clear()
+
+    def test_per_form_template_name(self):
         class Person(Form):
             first_name = CharField()
             template_name = "forms_tests/form_snippet.html"
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index 87084102a561..afa7a8a33d27 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -23,6 +23,7 @@
     all_valid,
     formset_factory,
 )
+from django.forms.renderers import TemplatesSetting
 from django.forms.utils import ErrorList
 from django.forms.widgets import HiddenInput
 from django.test import SimpleTestCase
@@ -1435,6 +1436,26 @@ def test_disable_delete_extra_formset_forms(self):
         self.assertIs(formset._should_delete_form(formset.forms[1]), False)
         self.assertIs(formset._should_delete_form(formset.forms[2]), False)

+    def test_template_name_uses_renderer_value(self):
+        class CustomRenderer(TemplatesSetting):
+            formset_template_name = "a/custom/formset/template.html"
+
+        ChoiceFormSet = formset_factory(Choice, renderer=CustomRenderer)
+
+        self.assertEqual(
+            ChoiceFormSet().template_name, "a/custom/formset/template.html"
+        )
+
+    def test_template_name_can_be_overridden(self):
+        class CustomFormSet(BaseFormSet):
+            template_name = "a/custom/formset/template.html"
+
+        ChoiceFormSet = formset_factory(Choice, formset=CustomFormSet)
+
+        self.assertEqual(
+            ChoiceFormSet().template_name, "a/custom/formset/template.html"
+        )
+
     def test_custom_renderer(self):
         """
         A custom renderer passed to a formset_factory() is passed to all forms

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:

diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 4d3cf8997d..a6b4d11f4a 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -527,12 +527,18 @@ a form object, and each rendering method returns a string.

 .. attribute:: Form.template_name

-The name of a template that is going to be rendered if the form is cast into a
-string, e.g. via ``print(form)`` or in a template via ``{{ form }}``. By
-default this template is ``'django/forms/default.html'``, which is a proxy for
-``'django/forms/table.html'``. The template can be changed per form by
-overriding the ``template_name`` attribute or more generally by overriding the
-default template, see also :ref:`overriding-built-in-form-templates`.
+The name of the template rendered if the form is cast into a string, e.g. via
+``print(form)`` or in a template via ``{{ form }}``.
+
+By default, a property returning the value of the renderer's
+:attr:`~django.forms.renderers.BaseRenderer.form_template_name`. You may set it
+as a string template name in order to override that for a particular form
+class.
+
+.. versionchanged:: 4.1
+
+    In older versions ``template_name`` defaulted to the string value
+    ``'django/forms/default.html'``.

 ``template_name_label``
 -----------------------

diff --git a/docs/ref/forms/renderers.txt b/docs/ref/forms/renderers.txt
index 9047e2343f..8c0263e051 100644
--- a/docs/ref/forms/renderers.txt
+++ b/docs/ref/forms/renderers.txt
@@ -26,9 +26,16 @@ A custom renderer can be specified by updating the :setting:`FORM_RENDERER`
 setting. It defaults to
 ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``.

-You can also provide a custom renderer by setting the
+By specifying a custom form renderer and overriding
+:attr:`~.BaseRenderer.form_template_name` you can adjust the default form
+markup across your project from a single place.
+
+You can also provide a custom renderer per-form or per-widget by setting the
 :attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
-of :meth:`.Widget.render`.
+of :meth:`.Form.render`, or :meth:`.Widget.render`.
+
+Matching points apply to formset rendering. See :ref:`formset-rendering` for
+discussion.

 Use one of the :ref:`built-in template form renderers
 <built-in-template-form-renderers>` or implement your own. Custom renderers
@@ -36,6 +43,38 @@ must implement a ``render(template_name, context, request=None)`` method. It
 should return a rendered templates (as a string) or raise
 :exc:`~django.template.TemplateDoesNotExist`.

+.. class:: BaseRenderer
+
+    The base class for the built-in form renderers.
+
+    .. attribute:: form_template_name
+
+        .. versionadded:: 4.1
+
+        The default name of the template to use to render a form.
+
+        Defaults to ``"django/forms/default.html"``, which is a proxy for
+        ``"django/forms/table.html"``.
+
+    .. attribute:: formset_template_name
+
+        .. versionadded:: 4.1
+
+        The default name of the template to use to render a formset.
+
+        Defaults to ``"django/forms/formsets/default.html"``, which is a proxy
+        for ``"django/forms/formsets/table.html"``.
+
+    .. method:: get_template(template_name)
+
+        Subclasses must implement this method with the appropriate template
+        finding logic.
+
+    .. method:: render(template_name, context, request=None)
+
+        Renders the given template, or raises
+        :exc:`~django.template.TemplateDoesNotExist`.
+
 .. _built-in-template-form-renderers:

 Built-in-template form renderers

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index b564a325a9..4cad556dc3 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -244,6 +244,20 @@ File Uploads
 Forms
 ~~~~~

+* The default template used to render forms when cast to a string, e.g. in
+  templates as ``{{ form }}``, is now configurable at the project-level by
+  setting :attr:`~django.forms.renderers.BaseRenderer.form_template_name` on
+  the class provided for :setting:`FORM_RENDERER`.
+
+  :attr:`.Form.template_name` is now a property deferring to the renderer, but
+  may be overridden with a string value to specify the template name per-form
+  class.
+
+  Similarly, the default template used to render formsets can be specified via
+  the matching
+  :attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
+  attribute.
+
 * The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field
   labels in ``<legend>`` tags via the new ``tag`` argument of
   :meth:`~django.forms.BoundField.label_tag`.

diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt
index a549a047cd..0206959f1d 100644
--- a/docs/topics/forms/formsets.txt
+++ b/docs/topics/forms/formsets.txt
@@ -783,11 +783,22 @@ Formsets have the following attributes and methods associated with rendering:

     .. versionadded:: 4.0

-    The name of the template used when calling ``__str__`` or :meth:`.render`.
-    This template renders the formset's management form and then each form in
-    the formset as per the template defined by the form's
-    :attr:`~django.forms.Form.template_name`. This is a proxy of ``as_table``
-    by default.
+    The name of the template rendered if the formset is cast into a string,
+    e.g. via ``print(formset)`` or in a template via ``{{ formset }}``.
+
+    By default, a property returning the value of the renderer's
+    :attr:`~django.forms.renderers.BaseRenderer.formset_template_name`. You may
+    set it as a string template name in order to override that for a particular
+    formset class.
+
+    This template will be used to render the formset's management form, and
+    then each form in the formset as per the template defined by the form's
+    :attr:`~django.forms.Form.template_name`.
+
+    .. versionchanged:: 4.1
+
+        In older versions ``template_name`` defaulted to the string value
+        ``'django/forms/formset/default.html'``.

 .. attribute:: BaseFormSet.template_name_p

diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index 2d0d61aa02..0e98e50692 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -759,10 +759,14 @@ Reusable form templates

 If your site uses the same rendering logic for forms in multiple places, you
 can reduce duplication by saving the form's loop in a standalone template and
-overriding the forms :attr:`~django.forms.Form.template_name` attribute to
-render the form using the custom template. The below example will result in
-``{{ form }}`` being rendered as the output of the ``form_snippet.html``
-template.
+setting a custom :setting:`FORM_RENDERER` to use that
+:attr:`~django.forms.renderers.BaseRenderer.form_template_name` site-wide. You
+can also customize per-form by overriding the form's
+:attr:`~django.forms.Form.template_name` attribute to render the form using the
+custom template.
+
+The below example will result in ``{{ form }}`` being rendered as the output of
+the ``form_snippet.html`` template.

 In your templates:

@@ -779,16 +783,42 @@ In your templates:
         </div>
     {% endfor %}

-In your form::
+Then you can configure the :setting:`FORM_RENDERER` setting:
+
+.. code-block:: python
+    :caption: settings.py
+
+    from django.forms.renderers import TemplatesSetting
+
+    class CustomFormRenderer(TemplatesSetting):
+        form_template_name = "form_snippet.html"
+
+    FORM_RENDERER = "project.settings.CustomFormRenderer"
+
+… or for a single form::

     class MyForm(forms.Form):
-        template_name = 'form_snippet.html'
+        template_name = "form_snippet.html"
         ...

+… or for a single render of a form instance, passing in the template name to
+the :meth:`.Form.render()`. Here's an example of this being used in a view::
+
+    def index(request):
+        form = MyForm()
+        rendered_form = form.render("form_snippet.html")
+        context = {'form': rendered_form}
+        return render(request, 'index.html', context)
+
 .. versionchanged:: 4.0

     Template rendering of forms was added.

+.. versionchanged:: 4.1
+
+    The ability to set the default ``form_template_name`` on the form renderer
+    was added.
+
 Further topics
 ==============


diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index 42d0d96ea6e0..4e3d025a569d 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -116,7 +116,7 @@ def get_deleted_objects(objs, request, admin_site):
         return [], {}, set(), []
     else:
         using = router.db_for_write(obj._meta.model)
-    collector = NestedObjects(using=using)
+    collector = NestedObjects(using=using, origin=objs)
     collector.collect(objs)
     perms_needed = set()

diff --git a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
index e2856bb0f77e..5593ecb46955 100644
--- a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
+++ b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
@@ -51,7 +51,7 @@ def handle(self, **options):
                     ct_info = []
                     for ct in to_remove:
                         ct_info.append('    - Content type for %s.%s' % (ct.app_label, ct.model))
-                        collector = NoFastDeleteCollector(using=using)
+                        collector = NoFastDeleteCollector(using=using, origin=ct)
                         collector.collect([ct])

                         for obj_type, objs in collector.data.items():
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 0d50dad0c98d..793cd936f198 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -987,7 +987,7 @@ def delete(self, using=None, keep_parents=False):
                 "to None." % (self._meta.object_name, self._meta.pk.attname)
             )
         using = using or router.db_for_write(self.__class__, instance=self)
-        collector = Collector(using=using)
+        collector = Collector(using=using, origin=self)
         collector.collect([self], keep_parents=keep_parents)
         return collector.delete()

diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py
index d8d8b2599033..b99337a309ae 100644
--- a/django/db/models/deletion.py
+++ b/django/db/models/deletion.py
@@ -76,8 +76,10 @@ def get_candidate_relations_to_delete(opts):

 class Collector:
-    def __init__(self, using):
+    def __init__(self, using, origin=None):
         self.using = using
+        # A Model or QuerySet object.
+        self.origin = origin
         # Initially, {model: {instances}}, later values become lists.
         self.data = defaultdict(set)
         # {model: {(field, value): {instances}}}
@@ -404,7 +406,8 @@ def delete(self):
             for model, obj in self.instances_with_model():
                 if not model._meta.auto_created:
                     signals.pre_delete.send(
-                        sender=model, instance=obj, using=self.using
+                        sender=model, instance=obj, using=self.using,
+                        origin=self.origin,
                     )

             # fast deletes
@@ -435,7 +438,8 @@ def delete(self):
                 if not model._meta.auto_created:
                     for obj in instances:
                         signals.post_delete.send(
-                            sender=model, instance=obj, using=self.using
+                            sender=model, instance=obj, using=self.using,
+                            origin=self.origin,
                         )

         # update collected instances
diff --git a/django/db/models/query.py b/django/db/models/query.py
index fb6639793ac9..86b1631f6707 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -753,7 +753,7 @@ def delete(self):
         del_query.query.select_related = False
         del_query.query.clear_ordering(force=True)

-        collector = Collector(using=del_query.db)
+        collector = Collector(using=del_query.db, origin=self)
         collector.collect(del_query)
         deleted, _rows_count = collector.delete()

diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
index 4eb55c906af9..6f0ce0527cc7 100644
--- a/docs/ref/signals.txt
+++ b/docs/ref/signals.txt
@@ -195,6 +195,12 @@ Arguments sent with this signal:
 ``using``
     The database alias being used.

+``origin``
+    .. versionadded:: 4.1
+
+    The origin of the deletion being the instance of a ``Model`` or
+    ``QuerySet`` class.
+
 ``post_delete``
 ---------------

@@ -219,6 +225,12 @@ Arguments sent with this signal:
 ``using``
     The database alias being used.

+``origin``
+    .. versionadded:: 4.1
+
+    The origin of the deletion being the instance of a ``Model`` or
+    ``QuerySet`` class.
+
 ``m2m_changed``
 ---------------

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 8cdec777f93a..fa1c05bc80fa 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -249,7 +249,9 @@ Serialization
 Signals
 ~~~~~~~

-* ...
+* The :data:`~django.db.models.signals.pre_delete` and
+  :data:`~django.db.models.signals.post_delete` signals now dispatch the
+  ``origin`` of the deletion.

 Templates
 ~~~~~~~~~

diff --git a/tests/signals/models.py b/tests/signals/models.py
index ca30cdc51f5a..b758244749ae 100644
--- a/tests/signals/models.py
+++ b/tests/signals/models.py
@@ -30,3 +30,8 @@ class Book(models.Model):

     def __str__(self):
         return self.name
+
+
+class Page(models.Model):
+    book = models.ForeignKey(Book, on_delete=models.CASCADE)
+    text = models.TextField()
diff --git a/tests/signals/tests.py b/tests/signals/tests.py
index c4c692ea3ed0..b89b7bd91c31 100644
--- a/tests/signals/tests.py
+++ b/tests/signals/tests.py
@@ -7,7 +7,7 @@
 from django.test import SimpleTestCase, TestCase
 from django.test.utils import isolate_apps

-from .models import Author, Book, Car, Person
+from .models import Author, Book, Car, Page, Person

 class BaseSignalSetup:
@@ -118,9 +118,9 @@ def post_save_handler(signal, sender, instance, **kwargs):
     def test_delete_signals(self):
         data = []

-        def pre_delete_handler(signal, sender, instance, **kwargs):
+        def pre_delete_handler(signal, sender, instance, origin, **kwargs):
             data.append(
-                (instance, sender, instance.id is None)
+                (instance, sender, instance.id is None, origin)
             )

         # #8285: signals can be any callable
@@ -128,9 +128,9 @@ class PostDeleteHandler:
             def __init__(self, data):
                 self.data = data

-            def __call__(self, signal, sender, instance, **kwargs):
+            def __call__(self, signal, sender, instance, origin, **kwargs):
                 self.data.append(
-                    (instance, sender, instance.id is None)
+                    (instance, sender, instance.id is None, origin)
                 )
         post_delete_handler = PostDeleteHandler(data)

@@ -140,8 +140,8 @@ def __call__(self, signal, sender, instance, **kwargs):
             p1 = Person.objects.create(first_name="John", last_name="Smith")
             p1.delete()
             self.assertEqual(data, [
-                (p1, Person, False),
-                (p1, Person, False),
+                (p1, Person, False, p1),
+                (p1, Person, False, p1),
             ])
             data[:] = []

@@ -152,8 +152,8 @@ def __call__(self, signal, sender, instance, **kwargs):
             p2.save()
             p2.delete()
             self.assertEqual(data, [
-                (p2, Person, False),
-                (p2, Person, False),
+                (p2, Person, False, p2),
+                (p2, Person, False, p2),
             ])
             data[:] = []

@@ -167,6 +167,78 @@ def __call__(self, signal, sender, instance, **kwargs):
             signals.pre_delete.disconnect(pre_delete_handler)
             signals.post_delete.disconnect(post_delete_handler)

+    def test_delete_signals_origin_model(self):
+        data = []
+
+        def pre_delete_handler(signal, sender, instance, origin, **kwargs):
+            data.append((sender, origin))
+
+        def post_delete_handler(signal, sender, instance, origin, **kwargs):
+            data.append((sender, origin))
+
+        person = Person.objects.create(first_name='John', last_name='Smith')
+        book = Book.objects.create(name='Rayuela')
+        Page.objects.create(text='Page 1', book=book)
+        Page.objects.create(text='Page 2', book=book)
+
+        signals.pre_delete.connect(pre_delete_handler, weak=False)
+        signals.post_delete.connect(post_delete_handler, weak=False)
+        try:
+            # Instance deletion.
+            person.delete()
+            self.assertEqual(data, [(Person, person), (Person, person)])
+            data[:] = []
+            # Cascade deletion.
+            book.delete()
+            self.assertEqual(data, [
+                (Page, book),
+                (Page, book),
+                (Book, book),
+                (Page, book),
+                (Page, book),
+                (Book, book),
+            ])
+        finally:
+            signals.pre_delete.disconnect(pre_delete_handler)
+            signals.post_delete.disconnect(post_delete_handler)
+
+    def test_delete_signals_origin_queryset(self):
+        data = []
+
+        def pre_delete_handler(signal, sender, instance, origin, **kwargs):
+            data.append((sender, origin))
+
+        def post_delete_handler(signal, sender, instance, origin, **kwargs):
+            data.append((sender, origin))
+
+        Person.objects.create(first_name='John', last_name='Smith')
+        book = Book.objects.create(name='Rayuela')
+        Page.objects.create(text='Page 1', book=book)
+        Page.objects.create(text='Page 2', book=book)
+
+        signals.pre_delete.connect(pre_delete_handler, weak=False)
+        signals.post_delete.connect(post_delete_handler, weak=False)
+        try:
+            # Queryset deletion.
+            qs = Person.objects.all()
+            qs.delete()
+            self.assertEqual(data, [(Person, qs), (Person, qs)])
+            data[:] = []
+            # Cascade deletion.
+            qs = Book.objects.all()
+            qs.delete()
+            self.assertEqual(data, [
+                (Page, qs),
+                (Page, qs),
+                (Book, qs),
+                (Page, qs),
+                (Page, qs),
+                (Book, qs),
+            ])
+        finally:
+            signals.pre_delete.disconnect(pre_delete_handler)
+            signals.post_delete.disconnect(post_delete_handler)
+
     def test_decorators(self):
         data = []

diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
index 4eb55c906a..6f0ce0527c 100644
--- a/docs/ref/signals.txt
+++ b/docs/ref/signals.txt
@@ -195,6 +195,12 @@ Arguments sent with this signal:
 ``using``
     The database alias being used.

+``origin``
+    .. versionadded:: 4.1
+
+    The origin of the deletion being the instance of a ``Model`` or
+    ``QuerySet`` class.
+
 ``post_delete``
 ---------------

@@ -219,6 +225,12 @@ Arguments sent with this signal:
 ``using``
     The database alias being used.

+``origin``
+    .. versionadded:: 4.1
+
+    The origin of the deletion being the instance of a ``Model`` or
+    ``QuerySet`` class.
+
 ``m2m_changed``
 ---------------

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 8cdec777f9..fa1c05bc80 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -249,7 +249,9 @@ Serialization
 Signals
 ~~~~~~~

-* ...
+* The :data:`~django.db.models.signals.pre_delete` and
+  :data:`~django.db.models.signals.post_delete` signals now dispatch the
+  ``origin`` of the deletion.

 Templates
 ~~~~~~~~~


diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 8a2fb944f226..51ddfc981922 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -419,12 +419,19 @@ def render(self, context):

 class NowNode(Node):
-    def __init__(self, format_string):
+    def __init__(self, format_string, asvar=None):
         self.format_string = format_string
+        self.asvar = asvar

     def render(self, context):
         tzinfo = timezone.get_current_timezone() if settings.USE_TZ else None
-        return date(datetime.now(tz=tzinfo), self.format_string)
+        formatted = date(datetime.now(tz=tzinfo), self.format_string)
+
+        if self.asvar:
+            context[self.asvar] = formatted
+            return ''
+        else:
+            return formatted

 class SpacelessNode(Node):
@@ -1200,10 +1207,14 @@ def now(parser, token):
         It is {% now "jS F Y H:i" %}
     """
     bits = token.split_contents()
+    asvar = None
+    if len(bits) == 4 and bits[-2] == 'as':
+        asvar = bits[-1]
+        bits = bits[:-2]
     if len(bits) != 2:
         raise TemplateSyntaxError("'now' statement takes one argument")
     format_string = bits[1][1:-1]
-    return NowNode(format_string)
+    return NowNode(format_string, asvar)

 @register.tag
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 2b3d48d90ef8..146f2eacd67e 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -799,6 +799,18 @@ This would display as "It is the 4th of September".

         It is {% now "SHORT_DATETIME_FORMAT" %}

+
+You can also use the syntax ``{% now "Y" as current_year %}`` to store the
+output inside a variable. This is useful if you want to use ``{% now %}``
+inside a template tag like :ttag:`blocktrans` for example::
+
+    {% now "Y" as current_year %}
+    {% blocktrans %}Copyright {{ current_year }}{% endblocktrans %}
+
+.. versionadded:: 1.8
+
+The ability to use the "as" syntax was added.
+
 .. templatetag:: regroup

 regroup
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index 73ad244cfbc3..b342234ef953 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -419,6 +419,9 @@ Templates
 * Added a :class:`locmem.Loader <django.template.loaders.locmem.Loader>`
   class that loads Django templates from a Python dictionary.

+* The :ttag:`now` tag can now store its output in a context variable with the
+  usual syntax: ``{% now 'j n Y' as varname %}``.
+
 Requests and Responses
 ^^^^^^^^^^^^^^^^^^^^^^

diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py
index 71da1ee40abf..67ac08601b4f 100644
--- a/tests/template_tests/tests.py
+++ b/tests/template_tests/tests.py
@@ -1697,6 +1697,8 @@ def get_template_tests(self):
                 datetime.now().day, datetime.now().month, datetime.now().year)),
             'now06': ('''{% now "j 'n' Y"%}''', {}, '''%d '%d' %d''' % (
                 datetime.now().day, datetime.now().month, datetime.now().year)),
+            'now07': ('''{% now "j n Y" as N %}-{{N}}-''', {}, '''-%d %d %d-''' % (
+                datetime.now().day, datetime.now().month, datetime.now().year)),

             ### URL TAG ########################################################
             # Successes

diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 2b3d48d90e..146f2eacd6 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -799,6 +799,18 @@ This would display as "It is the 4th of September".

         It is {% now "SHORT_DATETIME_FORMAT" %}

+
+You can also use the syntax ``{% now "Y" as current_year %}`` to store the
+output inside a variable. This is useful if you want to use ``{% now %}``
+inside a template tag like :ttag:`blocktrans` for example::
+
+    {% now "Y" as current_year %}
+    {% blocktrans %}Copyright {{ current_year }}{% endblocktrans %}
+
+.. versionadded:: 1.8
+
+The ability to use the "as" syntax was added.
+
 .. templatetag:: regroup

 regroup

diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index 73ad244cfb..b342234ef9 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -419,6 +419,9 @@ Templates
 * Added a :class:`locmem.Loader <django.template.loaders.locmem.Loader>`
   class that loads Django templates from a Python dictionary.

+* The :ttag:`now` tag can now store its output in a context variable with the
+  usual syntax: ``{% now 'j n Y' as varname %}``.
+
 Requests and Responses
 ^^^^^^^^^^^^^^^^^^^^^^


diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index fea271f40305..4d8ea3889838 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -2,7 +2,9 @@

 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured  # NOQA
 from django.db.models.query import Q, QuerySet, Prefetch  # NOQA
-from django.db.models.expressions import Expression, F, Value, Func, Case, When  # NOQA
+from django.db.models.expressions import (  # NOQA
+    Expression, ExpressionWrapper, F, Value, Func, Case, When,
+)
 from django.db.models.manager import Manager  # NOQA
 from django.db.models.base import Model  # NOQA
 from django.db.models.aggregates import *  # NOQA
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index c48204f4201c..c6959f87b883 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -126,12 +126,12 @@ class BaseExpression(object):
     # aggregate specific fields
     is_summary = False

-    def get_db_converters(self, connection):
-        return [self.convert_value] + self.output_field.get_db_converters(connection)
-
     def __init__(self, output_field=None):
         self._output_field = output_field

+    def get_db_converters(self, connection):
+        return [self.convert_value] + self.output_field.get_db_converters(connection)
+
     def get_source_expressions(self):
         return []

@@ -656,6 +656,29 @@ def get_group_by_cols(self):
         return [self]

+class ExpressionWrapper(Expression):
+    """
+    An expression that can wrap another expression so that it can provide
+    extra context to the inner expression, such as the output_field.
+    """
+
+    def __init__(self, expression, output_field):
+        super(ExpressionWrapper, self).__init__(output_field=output_field)
+        self.expression = expression
+
+    def set_source_expressions(self, exprs):
+        self.expression = exprs[0]
+
+    def get_source_expressions(self):
+        return [self.expression]
+
+    def as_sql(self, compiler, connection):
+        return self.expression.as_sql(compiler, connection)
+
+    def __repr__(self):
+        return "{}({})".format(self.__class__.__name__, self.expression)
+
+
 class When(Expression):
     template = 'WHEN %(condition)s THEN %(result)s'

diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt
index dfc852c0485a..b3d2e8b8c02f 100644
--- a/docs/ref/models/expressions.txt
+++ b/docs/ref/models/expressions.txt
@@ -161,6 +161,27 @@ values, rather than on Python values.
 This is documented in :ref:`using F() expressions in queries
 <using-f-expressions-in-filters>`.

+.. _using-f-with-annotations:
+
+Using ``F()`` with annotations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``F()`` can be used to create dynamic fields on your models by combining
+different fields with arithmetic::
+
+    company = Company.objects.annotate(
+        chairs_needed=F('num_employees') - F('num_chairs'))
+
+If the fields that you're combining are of different types you'll need
+to tell Django what kind of field will be returned. Since ``F()`` does not
+directly support ``output_field`` you will need to wrap the expression with
+:class:`ExpressionWrapper`::
+
+    from django.db.models import DateTimeField, ExpressionWrapper, F
+
+    Ticket.objects.annotate(
+        expires=ExpressionWrapper(
+            F('active_at') + F('duration'), output_field=DateTimeField()))

 .. _func-expressions:

@@ -274,17 +295,6 @@ should define the desired ``output_field``. For example, adding an
 ``IntegerField()`` and a ``FloatField()`` together should probably have
 ``output_field=FloatField()`` defined.

-.. note::
-
-    When you need to define the ``output_field`` for ``F`` expression
-    arithmetic between different types, it's necessary to surround the
-    expression in another expression::
-
-        from django.db.models import DateTimeField, Expression, F
-
-        Race.objects.annotate(finish=Expression(
-            F('start') + F('duration'), output_field=DateTimeField()))
-
 .. versionchanged:: 1.8

     ``output_field`` is a new parameter.
@@ -343,6 +353,19 @@ instantiating the model field as any arguments relating to data validation
 (``max_length``, ``max_digits``, etc.) will not be enforced on the expression's
 output value.

+``ExpressionWrapper()`` expressions
+-----------------------------------
+
+.. class:: ExpressionWrapper(expression, output_field)
+
+.. versionadded:: 1.8
+
+``ExpressionWrapper`` simply surrounds another expression and provides access
+to properties, such as ``output_field``, that may not be available on other
+expressions. ``ExpressionWrapper`` is necessary when using arithmetic on
+``F()`` expressions with different types as described in
+:ref:`using-f-with-annotations`.
+
 Conditional expressions
 -----------------------

diff --git a/tests/annotations/models.py b/tests/annotations/models.py
index 0c438b99f805..954c8ad33960 100644
--- a/tests/annotations/models.py
+++ b/tests/annotations/models.py
@@ -84,3 +84,12 @@ def __str__(self):
         return ('Company(name=%s, motto=%s, ticker_name=%s, description=%s)'
             % (self.name, self.motto, self.ticker_name, self.description)
         )
+
+
+@python_2_unicode_compatible
+class Ticket(models.Model):
+    active_at = models.DateTimeField()
+    duration = models.DurationField()
+
+    def __str__(self):
+        return '{} - {}'.format(self.active_at, self.duration)
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
index 45da5e49de4a..1230d5c52a2a 100644
--- a/tests/annotations/tests.py
+++ b/tests/annotations/tests.py
@@ -5,13 +5,14 @@

 from django.core.exceptions import FieldDoesNotExist, FieldError
 from django.db.models import (
-    F, BooleanField, CharField, Count, Func, IntegerField, Sum, Value,
+    F, BooleanField, CharField, Count, DateTimeField, ExpressionWrapper, Func,
+    IntegerField, Sum, Value,
 )
 from django.test import TestCase
 from django.utils import six

 from .models import (
-    Author, Book, Company, DepartmentStore, Employee, Publisher, Store,
+    Author, Book, Company, DepartmentStore, Employee, Publisher, Store, Ticket,
 )

@@ -135,6 +136,24 @@ def test_joined_annotation(self):
         for book in books:
             self.assertEqual(book.num_awards, book.publisher.num_awards)

+    def test_mixed_type_annotation_date_interval(self):
+        active = datetime.datetime(2015, 3, 20, 14, 0, 0)
+        duration = datetime.timedelta(hours=1)
+        expires = datetime.datetime(2015, 3, 20, 14, 0, 0) + duration
+        Ticket.objects.create(active_at=active, duration=duration)
+        t = Ticket.objects.annotate(
+            expires=ExpressionWrapper(F('active_at') + F('duration'), output_field=DateTimeField())
+        ).first()
+        self.assertEqual(t.expires, expires)
+
+    def test_mixed_type_annotation_numbers(self):
+        test = self.b1
+        b = Book.objects.annotate(
+            combined=ExpressionWrapper(F('pages') + F('rating'), output_field=IntegerField())
+        ).get(isbn=test.isbn)
+        combined = int(test.pages + test.rating)
+        self.assertEqual(b.combined, combined)
+
     def test_annotate_with_aggregation(self):
         books = Book.objects.annotate(
             is_book=Value(1, output_field=IntegerField()),
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index ae2134e96489..3e26be6d0bc0 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -11,8 +11,8 @@
     Avg, Count, Max, Min, StdDev, Sum, Variance,
 )
 from django.db.models.expressions import (
-    F, Case, Col, Date, DateTime, Func, OrderBy, Random, RawSQL, Ref, Value,
-    When,
+    F, Case, Col, Date, DateTime, ExpressionWrapper, Func, OrderBy, Random,
+    RawSQL, Ref, Value, When,
 )
 from django.db.models.functions import (
     Coalesce, Concat, Length, Lower, Substr, Upper,
@@ -855,6 +855,10 @@ def test_expressions(self):
         self.assertEqual(repr(DateTime('published', 'exact', utc)), "DateTime(published, exact, %s)" % utc)
         self.assertEqual(repr(F('published')), "F(published)")
         self.assertEqual(repr(F('cost') + F('tax')), "<CombinedExpression: F(cost) + F(tax)>")
+        self.assertEqual(
+            repr(ExpressionWrapper(F('cost') + F('tax'), models.IntegerField())),
+            "ExpressionWrapper(F(cost) + F(tax))"
+        )
         self.assertEqual(repr(Func('published', function='TO_CHAR')), "Func(F(published), function=TO_CHAR)")
         self.assertEqual(repr(OrderBy(Value(1))), 'OrderBy(Value(1), descending=False)')
         self.assertEqual(repr(Random()), "Random()")

diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt
index dfc852c048..b3d2e8b8c0 100644
--- a/docs/ref/models/expressions.txt
+++ b/docs/ref/models/expressions.txt
@@ -161,6 +161,27 @@ values, rather than on Python values.
 This is documented in :ref:`using F() expressions in queries
 <using-f-expressions-in-filters>`.

+.. _using-f-with-annotations:
+
+Using ``F()`` with annotations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``F()`` can be used to create dynamic fields on your models by combining
+different fields with arithmetic::
+
+    company = Company.objects.annotate(
+        chairs_needed=F('num_employees') - F('num_chairs'))
+
+If the fields that you're combining are of different types you'll need
+to tell Django what kind of field will be returned. Since ``F()`` does not
+directly support ``output_field`` you will need to wrap the expression with
+:class:`ExpressionWrapper`::
+
+    from django.db.models import DateTimeField, ExpressionWrapper, F
+
+    Ticket.objects.annotate(
+        expires=ExpressionWrapper(
+            F('active_at') + F('duration'), output_field=DateTimeField()))

 .. _func-expressions:

@@ -274,17 +295,6 @@ should define the desired ``output_field``. For example, adding an
 ``IntegerField()`` and a ``FloatField()`` together should probably have
 ``output_field=FloatField()`` defined.

-.. note::
-
-    When you need to define the ``output_field`` for ``F`` expression
-    arithmetic between different types, it's necessary to surround the
-    expression in another expression::
-
-        from django.db.models import DateTimeField, Expression, F
-
-        Race.objects.annotate(finish=Expression(
-            F('start') + F('duration'), output_field=DateTimeField()))
-
 .. versionchanged:: 1.8

     ``output_field`` is a new parameter.
@@ -343,6 +353,19 @@ instantiating the model field as any arguments relating to data validation
 (``max_length``, ``max_digits``, etc.) will not be enforced on the expression's
 output value.

+``ExpressionWrapper()`` expressions
+-----------------------------------
+
+.. class:: ExpressionWrapper(expression, output_field)
+
+.. versionadded:: 1.8
+
+``ExpressionWrapper`` simply surrounds another expression and provides access
+to properties, such as ``output_field``, that may not be available on other
+expressions. ``ExpressionWrapper`` is necessary when using arithmetic on
+``F()`` expressions with different types as described in
+:ref:`using-f-with-annotations`.
+
 Conditional expressions
 -----------------------


diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py
index 4d26bfe278a3..4349f33a61d6 100644
--- a/django/core/management/commands/makemigrations.py
+++ b/django/core/management/commands/makemigrations.py
@@ -57,9 +57,20 @@ def add_arguments(self, parser):
             '--check', action='store_true', dest='check_changes',
             help='Exit with a non-zero status if model changes are missing migrations.',
         )
+        parser.add_argument(
+            '--scriptable', action='store_true', dest='scriptable',
+            help=(
+                'Divert log output and input prompts to stderr, writing only '
+                'paths of generated migration files to stdout.'
+            ),
+        )
+
+    @property
+    def log_output(self):
+        return self.stderr if self.scriptable else self.stdout

     def log(self, msg):
-        self.stdout.write(msg)
+        self.log_output.write(msg)

     @no_translations
     def handle(self, *app_labels, **options):
@@ -73,6 +84,10 @@ def handle(self, *app_labels, **options):
             raise CommandError('The migration name must be a valid Python identifier.')
         self.include_header = options['include_header']
         check_changes = options['check_changes']
+        self.scriptable = options['scriptable']
+        # If logs and prompts are diverted to stderr, remove the ERROR style.
+        if self.scriptable:
+            self.stderr.style_func = None

         # Make sure the app they asked for exists
         app_labels = set(app_labels)
@@ -147,7 +162,7 @@ def handle(self, *app_labels, **options):
             questioner = InteractiveMigrationQuestioner(
                 specified_apps=app_labels,
                 dry_run=self.dry_run,
-                prompt_output=self.stdout,
+                prompt_output=self.log_output,
             )
         else:
             questioner = NonInteractiveMigrationQuestioner(
@@ -226,6 +241,8 @@ def write_migration_files(self, changes):
                     self.log('  %s\n' % self.style.MIGRATE_LABEL(migration_string))
                     for operation in migration.operations:
                         self.log('    - %s' % operation.describe())
+                    if self.scriptable:
+                        self.stdout.write(migration_string)
                 if not self.dry_run:
                     # Write the migrations file to the disk.
                     migrations_directory = os.path.dirname(writer.path)
@@ -254,7 +271,7 @@ def handle_merge(self, loader, conflicts):
         if it's safe; otherwise, advises on how to fix it.
         """
         if self.interactive:
-            questioner = InteractiveMigrationQuestioner(prompt_output=self.stdout)
+            questioner = InteractiveMigrationQuestioner(prompt_output=self.log_output)
         else:
             questioner = MigrationQuestioner(defaults={'ask_merge': True})

@@ -327,6 +344,8 @@ def all_items_equal(seq):
                         fh.write(writer.as_string())
                     if self.verbosity > 0:
                         self.log('\nCreated new merge migration %s' % writer.path)
+                        if self.scriptable:
+                            self.stdout.write(writer.path)
                 elif self.verbosity == 3:
                     # Alternatively, makemigrations --merge --dry-run --verbosity 3
                     # will log the merge migrations rather than saving the file
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 6188324d5244..78770fbf72f3 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -825,6 +825,13 @@ Generate migration files without Django version and timestamp header.
 Makes ``makemigrations`` exit with a non-zero status when model changes without
 migrations are detected.

+.. django-admin-option:: --scriptable
+
+.. versionadded:: 4.1
+
+Diverts log output and input prompts to ``stderr``, writing only paths of
+generated migration files to ``stdout``.
+
 ``migrate``
 -----------

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 219201fd00d5..8cdec777f93a 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -210,6 +210,10 @@ Management Commands
 * :option:`makemigrations --no-input` now logs default answers and reasons why
   migrations cannot be created.

+* The new :option:`makemigrations --scriptable` options diverts log output and
+  input prompts to ``stderr``, writing only paths of generated migration files
+  to ``stdout``.
+
 Migrations
 ~~~~~~~~~~

diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index ef5db241d65d..f274c8486b48 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -1667,6 +1667,47 @@ class Meta:
         self.assertIn("model_name='sillymodel',", out.getvalue())
         self.assertIn("name='silly_char',", out.getvalue())

+    def test_makemigrations_scriptable(self):
+        """
+        With scriptable=True, log output is diverted to stderr, and only the
+        paths of generated migration files are written to stdout.
+        """
+        out = io.StringIO()
+        err = io.StringIO()
+        with self.temporary_migration_module(
+            module='migrations.migrations.test_migrations',
+        ) as migration_dir:
+            call_command(
+                'makemigrations',
+                'migrations',
+                scriptable=True,
+                stdout=out,
+                stderr=err,
+            )
+        initial_file = os.path.join(migration_dir, '0001_initial.py')
+        self.assertEqual(out.getvalue(), f'{initial_file}\n')
+        self.assertIn('    - Create model ModelWithCustomBase\n', err.getvalue())
+
+    @mock.patch('builtins.input', return_value='Y')
+    def test_makemigrations_scriptable_merge(self, mock_input):
+        out = io.StringIO()
+        err = io.StringIO()
+        with self.temporary_migration_module(
+            module='migrations.test_migrations_conflict',
+        ) as migration_dir:
+            call_command(
+                'makemigrations',
+                'migrations',
+                merge=True,
+                name='merge',
+                scriptable=True,
+                stdout=out,
+                stderr=err,
+            )
+        merge_file = os.path.join(migration_dir, '0003_merge.py')
+        self.assertEqual(out.getvalue(), f'{merge_file}\n')
+        self.assertIn(f'Created new merge migration {merge_file}', err.getvalue())
+
     def test_makemigrations_migrations_modules_path_not_exist(self):
         """
         makemigrations creates migrations when specifying a custom location

diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 6188324d52..78770fbf72 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -825,6 +825,13 @@ Generate migration files without Django version and timestamp header.
 Makes ``makemigrations`` exit with a non-zero status when model changes without
 migrations are detected.

+.. django-admin-option:: --scriptable
+
+.. versionadded:: 4.1
+
+Diverts log output and input prompts to ``stderr``, writing only paths of
+generated migration files to ``stdout``.
+
 ``migrate``
 -----------

diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 219201fd00..8cdec777f9 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -210,6 +210,10 @@ Management Commands
 * :option:`makemigrations --no-input` now logs default answers and reasons why
   migrations cannot be created.

+* The new :option:`makemigrations --scriptable` options diverts log output and
+  input prompts to ``stderr``, writing only paths of generated migration files
+  to ``stdout``.
+
 Migrations
 ~~~~~~~~~~