diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index 88e422e40ff..2337fe62a52 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -11,7 +11,7 @@ from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import Collector
from django.db.models.sql.constants import QUERY_TERMS
-from django.forms.forms import pretty_name
+from django.forms.utils import pretty_name
from django.utils import formats, six, timezone
from django.utils.encoding import force_str, force_text, smart_text
from django.utils.html import format_html
diff --git a/django/forms/__init__.py b/django/forms/__init__.py
index e0be2d164fa..1c319219a63 100644
--- a/django/forms/__init__.py
+++ b/django/forms/__init__.py
@@ -3,6 +3,7 @@ Django validation and HTML form handling.
"""
from django.core.exceptions import ValidationError # NOQA
+from django.forms.boundfield import * # NOQA
from django.forms.fields import * # NOQA
from django.forms.forms import * # NOQA
from django.forms.formsets import * # NOQA
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
new file mode 100644
index 00000000000..bcfe2e2a2d1
--- /dev/null
+++ b/django/forms/boundfield.py
@@ -0,0 +1,227 @@
+from __future__ import unicode_literals
+
+import datetime
+
+from django.forms.utils import flatatt, pretty_name
+from django.forms.widgets import Textarea, TextInput
+from django.utils import six
+from django.utils.encoding import (
+ force_text, python_2_unicode_compatible, smart_text,
+)
+from django.utils.html import conditional_escape, format_html, html_safe
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+__all__ = ('BoundField',)
+
+
+UNSET = object()
+
+
+@html_safe
+@python_2_unicode_compatible
+class BoundField(object):
+ "A Field plus data"
+ def __init__(self, form, field, name):
+ self.form = form
+ self.field = field
+ self.name = name
+ self.html_name = form.add_prefix(name)
+ self.html_initial_name = form.add_initial_prefix(name)
+ self.html_initial_id = form.add_initial_prefix(self.auto_id)
+ if self.field.label is None:
+ self.label = pretty_name(name)
+ else:
+ self.label = self.field.label
+ self.help_text = field.help_text or ''
+ self._initial_value = UNSET
+
+ def __str__(self):
+ """Renders this field as an HTML widget."""
+ if self.field.show_hidden_initial:
+ return self.as_widget() + self.as_hidden(only_initial=True)
+ return self.as_widget()
+
+ def __iter__(self):
+ """
+ Yields rendered strings that comprise all widgets in this BoundField.
+
+ This really is only useful for RadioSelect widgets, so that you can
+ iterate over individual radio buttons in a template.
+ """
+ id_ = self.field.widget.attrs.get('id') or self.auto_id
+ attrs = {'id': id_} if id_ else {}
+ for subwidget in self.field.widget.subwidgets(self.html_name, self.value(), attrs):
+ yield subwidget
+
+ def __len__(self):
+ return len(list(self.__iter__()))
+
+ def __getitem__(self, idx):
+ # Prevent unnecessary reevaluation when accessing BoundField's attrs
+ # from templates.
+ if not isinstance(idx, six.integer_types):
+ raise TypeError
+ return list(self.__iter__())[idx]
+
+ @property
+ def errors(self):
+ """
+ Returns an ErrorList for this field. Returns an empty ErrorList
+ if there are none.
+ """
+ return self.form.errors.get(self.name, self.form.error_class())
+
+ def as_widget(self, widget=None, attrs=None, only_initial=False):
+ """
+ Renders the field by rendering the passed widget, adding any HTML
+ attributes passed as attrs. If no widget is specified, then the
+ field's default widget will be used.
+ """
+ if not widget:
+ widget = self.field.widget
+
+ if self.field.localize:
+ widget.is_localized = True
+
+ attrs = attrs or {}
+ if self.field.disabled:
+ attrs['disabled'] = True
+ auto_id = self.auto_id
+ if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
+ if not only_initial:
+ attrs['id'] = auto_id
+ else:
+ attrs['id'] = self.html_initial_id
+
+ if not only_initial:
+ name = self.html_name
+ else:
+ name = self.html_initial_name
+ return force_text(widget.render(name, self.value(), attrs=attrs))
+
+ def as_text(self, attrs=None, **kwargs):
+ """
+ Returns a string of HTML for representing this as an .
+ """
+ return self.as_widget(TextInput(), attrs, **kwargs)
+
+ def as_textarea(self, attrs=None, **kwargs):
+ "Returns a string of HTML for representing this as a