diff --git a/django/forms/models.py b/django/forms/models.py
index 4d56f1d..0686525 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -5,7 +5,8 @@ and database field objects.
 
 from __future__ import absolute_import, unicode_literals
 
-from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
+from django.core.exceptions import (
+    ValidationError, NON_FIELD_ERRORS, FieldError, ImproperlyConfigured)
 from django.core.validators import EMPTY_VALUES
 from django.forms.fields import Field, ChoiceField
 from django.forms.forms import BaseForm, get_declared_fields
@@ -131,7 +132,7 @@ def model_to_dict(instance, fields=None, exclude=None):
             data[f.name] = f.value_from_object(instance)
     return data
 
-def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None):
+def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, overrides=None):
     """
     Returns a ``SortedDict`` containing form fields for the given model.
 
@@ -144,6 +145,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
     """
     field_list = []
     ignored = []
+    applied_overrides = []
     opts = model._meta
     for f in sorted(opts.fields + opts.many_to_many):
         if not f.editable:
@@ -157,6 +159,14 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
         else:
             kwargs = {}
 
+        if overrides:
+            for key, kwargs_overrides in overrides.iteritems():
+                if (key == f.name or
+                    (isinstance(key, type) and isinstance(f, key)) or
+                    (callable(key) and key(f))):
+                    kwargs.update(kwargs_overrides)
+                    applied_overrides.append(key)
+
         if formfield_callback is None:
             formfield = f.formfield(**kwargs)
         elif not callable(formfield_callback):
@@ -168,6 +178,18 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
             field_list.append((f.name, formfield))
         else:
             ignored.append(f.name)
+
+    if overrides:
+        for key in overrides.keys():
+            if not key in applied_overrides:
+                if isinstance(key, (type, basestring)):
+                    # TBC: should these pass silently?
+                    raise ImproperlyConfigured(
+                        'ModelForm.Meta.overrides contained unused key: %r' %
+                        key)
+                else:
+                    pass # TBC: ignore callables either way?
+
     field_dict = SortedDict(field_list)
     if fields:
         field_dict = SortedDict(
@@ -182,6 +204,7 @@ class ModelFormOptions(object):
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
         self.widgets = getattr(options, 'widgets', None)
+        self.overrides = getattr(options, 'overrides', None)
 
 
 class ModelFormMetaclass(type):
@@ -203,8 +226,9 @@ class ModelFormMetaclass(type):
         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
         if opts.model:
             # If a model is defined, extract form fields from it.
-            fields = fields_for_model(opts.model, opts.fields,
-                                      opts.exclude, opts.widgets, formfield_callback)
+            fields = fields_for_model(
+                opts.model, opts.fields, opts.exclude, opts.widgets,
+                formfield_callback, overrides=opts.overrides)
             # make sure opts.fields doesn't specify an invalid field
             none_model_fields = [k for k, v in fields.iteritems() if not v]
             missing_fields = set(none_model_fields) - \
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 0ca3774..85c3e3a 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -388,10 +388,30 @@ widget::
 The ``widgets`` dictionary accepts either widget instances (e.g.,
 ``Textarea(...)``) or classes (e.g., ``Textarea``).
 
+.. versionadded:: 1.5
+
 If you want to further customize a field -- including its type, label, etc. --
-you can do this by declaratively specifying fields like you would in a regular
-``Form``. Declared fields will override the default ones generated by using the
-``model`` attribute.
+you can use the ``overrides`` attribute of the inner ``Meta`` class. Using
+this you can not only change the widget [#]_ but also override any other
+keyword argument that the form field's ``__init__`` accepts, such as
+``label``, ``help_text``, ``widget``, ``required``, ``form_class``, etc. The
+``overrides`` attribute is a dictionary with keys which can be either:
+
+* a field name such as ``'name'``, ``'birth_date'`` to override a specific
+  field, or
+* a model field class as ``models.CharField``, ``models.FloatField`` to
+  override all fields of a particular type, or
+* a callable to perform more complex matching, such as
+  ``lambda f: f.name.startswith('question_')``
+
+The values of the ``overrides`` dictionary are simply dictionaries of keyword
+arguments that are passed to :meth:`~django.forms.Field.__init__` (or subclass thereof).
+
+.. [#] FIXME: "There should be one-- and preferably only one --obvious way to do it."
+
+You can also completely override form fields by declaratively specifying fields like
+you would in a regular ``Form``. Declared fields will override the default ones
+generated by using the ``model`` attribute.
 
 For example, if you wanted to use ``MyDateFormField`` for the ``pub_date``
 field, you could do the following::
diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py
index 18e6ddc..ffc3d32 100644
--- a/tests/regressiontests/forms/models.py
+++ b/tests/regressiontests/forms/models.py
@@ -74,4 +74,27 @@ class Group(models.Model):
 
 
 class Cheese(models.Model):
-   name = models.CharField(max_length=100)
+    name = models.CharField(max_length=100)
+
+
+class WalkCategory(models.Model):
+    name = models.CharField(max_length=100)
+
+    def __unicode__(self):
+        return self.name
+
+
+class Walk(models.Model):
+    name = models.CharField(max_length=255)
+    silliness = models.FloatField(
+        help_text='0 = not silly at all, 1 = completely silly')
+    description = models.CharField(max_length=1024, blank=True)
+    difficulty = models.PositiveSmallIntegerField(
+        choices=tuple(
+            enumerate(['Easy', 'Moderate', 'Difficult', 'Impossible'])
+        )
+    )
+    category = models.ForeignKey(WalkCategory)
+
+    def __unicode__(self):
+        return self.name
diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
index 8e2150c..1784cb1 100644
--- a/tests/regressiontests/forms/tests/__init__.py
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -13,7 +13,7 @@ from .input_formats import (LocalizedTimeTests, CustomTimeInputFormatsTests,
     CustomDateTimeInputFormatsTests, SimpleDateTimeFormatTests)
 from .media import FormsMediaTestCase, StaticFormsMediaTestCase
 from .models import (TestTicket12510, ModelFormCallableModelDefault,
-    FormsModelTestCase, RelatedModelFormTests)
+    FormsModelTestCase, RelatedModelFormTests, OverridesTest)
 from .regressions import FormsRegressionsTestCase
 from .util import FormsUtilTestCase
 from .validators import TestFieldWithValidators
diff --git a/tests/regressiontests/forms/tests/models.py b/tests/regressiontests/forms/tests/models.py
index 7687335..204b125 100644
--- a/tests/regressiontests/forms/tests/models.py
+++ b/tests/regressiontests/forms/tests/models.py
@@ -5,13 +5,15 @@ import datetime
 
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.db import models
-from django.forms import Form, ModelForm, FileField, ModelChoiceField
+from django.db.models.fields import BLANK_CHOICE_DASH
+from django.forms import Form, ModelForm, FileField, ModelChoiceField, widgets
 from django.forms.models import ModelFormMetaclass
 from django.test import TestCase
 from django.utils import six
+from django.utils.encoding import smart_unicode
 
 from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group,
-    BoundaryModel, Defaults)
+    BoundaryModel, Defaults, Walk, WalkCategory)
 
 
 class ChoiceFieldForm(ModelForm):
@@ -197,3 +199,149 @@ class RelatedModelFormTests(TestCase):
             model=A
 
         self.assertTrue(issubclass(ModelFormMetaclass(b'Form', (ModelForm,), {'Meta': Meta}), ModelForm))
+
+
+class WalkForm(ModelForm):
+    class Meta:
+        model = Walk
+
+class StrictWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            'description': {
+                'required': True,
+            },
+        }
+
+class TextareaWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            (lambda f: getattr(f, 'max_length', 0) > 300): {
+                'widget': widgets.Textarea,
+            },
+        }
+
+class HelpfulWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            'silliness': {
+                'help_text': 'Range: 0.0 -> 1.0',
+            },
+            'description': {
+                'help_text': '(optional)',
+            },
+            'category': {
+                'empty_label': '--[ choose one ]--',
+            },
+        }
+
+class ShortWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            'name': {
+                'label': 'Short name',
+                'max_length': 50,
+            },
+        }
+
+class RelaxedWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            'name': {
+                'required': False,
+            },
+        }
+
+class ModelChoiceFieldWithKeys(ModelChoiceField):
+    def label_from_instance(self, obj):
+        return u'[%s] %s' % (
+            smart_unicode(obj.pk),
+            smart_unicode(obj))
+
+class FancyWalkForm(WalkForm):
+    class Meta(WalkForm.Meta):
+        overrides = {
+            'category': {
+                'form_class': ModelChoiceFieldWithKeys,
+            },
+        }
+
+
+class OverridesTest(TestCase):
+    def setUp(self):
+        WalkCategory.objects.create(id=1, name='Long-legged')
+        WalkCategory.objects.create(id=2, name='All-fours')
+        self.normal_form = WalkForm()
+        self.strict_form = StrictWalkForm()
+        self.textarea_form = TextareaWalkForm()
+        self.short_form = ShortWalkForm()
+        self.relaxed_form = RelaxedWalkForm()
+        self.help_form = HelpfulWalkForm()
+        self.fancy_form = FancyWalkForm()
+
+    def test_required_false(self):
+        self.assertTrue(self.normal_form.fields['name'].required)
+        self.assertTrue(self.normal_form.fields['name'].widget.is_required)
+
+        self.assertFalse(self.relaxed_form.fields['name'].required)
+        self.assertFalse(self.relaxed_form.fields['name'].widget.is_required)
+
+    def test_required_true(self):
+        self.assertFalse(
+            self.normal_form.fields['description'].required)
+        self.assertFalse(
+            self.normal_form.fields['description'].widget.is_required)
+
+        self.assertTrue(
+            self.strict_form.fields['description'].required)
+        self.assertTrue(
+            self.strict_form.fields['description'].widget.is_required)
+
+    def test_max_length(self):
+        self.assertEqual(self.normal_form.fields['name'].max_length, 255)
+        self.assertEqual(
+            self.normal_form.fields['name'].widget.attrs['maxlength'], '255')
+
+        self.assertEqual(self.short_form.fields['name'].max_length, 50)
+        self.assertEqual(
+            self.short_form.fields['name'].widget.attrs['maxlength'], '50')
+
+    def test_textarea(self):
+        self.assertEqual(
+            type(self.normal_form.fields['description'].widget),
+            widgets.TextInput)
+        self.assertEqual(
+            type(self.textarea_form.fields['description'].widget),
+            widgets.Textarea)
+
+    def test_helptext(self):
+        self.assertRegexpMatches(
+            self.normal_form.fields['silliness'].help_text, r'^0 = not silly')
+        self.assertRegexpMatches(
+            self.help_form.fields['silliness'].help_text, r'^Range: 0')
+
+        self.assertEqual(
+            self.normal_form.fields['description'].help_text, '')
+        self.assertEqual(
+            self.help_form.fields['description'].help_text, '(optional)')
+
+    def test_empty_label(self):
+        self.assertRegexpMatches(
+            self.normal_form.fields['category'].empty_label, r'^\-+$')
+        self.assertEqual(
+            self.help_form.fields['category'].empty_label,
+            '--[ choose one ]--')
+
+    def test_label(self):
+        self.assertEqual(self.normal_form.fields['name'].label, 'Name')
+        self.assertEqual(self.short_form.fields['name'].label, 'Short name')
+
+    def test_form_class(self):
+        self.assertEqual(
+            list(self.normal_form.fields['category'].choices)[1:],
+            [(1, u'Long-legged'), (2, u'All-fours')]
+        )
+        self.assertEqual(
+            list(self.fancy_form.fields['category'].choices)[1:],
+            [(1, u'[1] Long-legged'), (2, u'[2] All-fours')]
+        )
