diff --git a/django/forms/forms.py b/django/forms/forms.py
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -59,6 +59,20 @@
     Option ``exclude`` is an optional list of field names. If provided,
     the named fields will be excluded from the returned fields, even if they
     are listed in the ``fields`` argument.
+
+    Option ``fieldsets`` is a list of two-tuples. The first item in each
+    two-tuple is a name for the fieldset, and the second is a dictionary
+    of fieldset options. Valid fieldset options in the dictionary include:
+
+    ``fields`` (required): A tuple of field names to display in the fieldset.
+
+    ``attrs``: A dictionary of HTML attributes to be used with the fieldset.
+
+    ``legend``: A string that should be the content of a ``legend`` tag
+    to open the fieldset.
+
+    ``description``: A string of optional extra text.
+
     """
     selected_fields = SortedDict()
     for field_name, field in fields.items():
@@ -66,6 +80,14 @@
             continue
         if opts.exclude and field_name in opts.exclude:
             continue
+        if opts.fieldsets:
+            for fieldset_name, fieldset_options in opts.fieldsets:
+                if field_name in fieldset_options['fields']:
+                    # Stop iteration of fieldsets so else condition is not used.
+                    break
+            else:
+                # Field is not used in any fieldset.
+                continue
         selected_fields[field_name] = field
     if opts.fields:
         selected_fields = SortedDict((field_name, selected_fields.get(field_name))
@@ -76,6 +98,7 @@
     def __init__(self, options=None):
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
+        self.fieldsets = getattr(options, 'fieldsets', None)
 
 class FormMetaclass(type):
     """
@@ -134,6 +157,23 @@
             raise KeyError('Key %r not found in Form' % name)
         return BoundField(self, field, name)
 
+    def _has_fieldsets(self):
+        """
+        Returns True if form has fieldsets. Depends on _meta attribute giving
+        options which have to define fieldsets. If you do not use FormMetaclass
+        and want to use this method, you have to define options otherwise. 
+        """
+        return self._meta.fieldsets is not None
+    has_fieldsets = property(_has_fieldsets)
+
+    def fieldsets(self):
+        """
+        Returns collection of FieldSets. Depends on _meta attribute giving
+        options which have to define fieldsets. If you do not use FormMetaclass
+        and want to use this method, you have to define options otherwise. 
+        """
+        return FieldSetCollection(self, self._meta.fieldsets)
+
     def _get_errors(self):
         "Returns an ErrorDict for the data provided for the form"
         if self._errors is None:
@@ -414,6 +454,58 @@
     # BaseForm itself has no way of designating self.fields.
     __metaclass__ = FormMetaclass
 
+class FieldSetCollection(object):
+    "A collection of FieldSets."
+    def __init__(self, form, fieldsets):
+        self.form = form
+        self.fieldsets = fieldsets
+
+    def __iter__(self):
+        if self.form.has_fieldsets:
+            for name, options in self.fieldsets:
+                yield self._construct_fieldset(name, options)
+        else:
+            # Construct dummy fieldset for form without any.
+            yield FieldSet(self.form, None, self.form.fields)
+
+    def __getitem__(self, key):
+        if self.form.has_fieldsets:
+            for name, options in self.fieldsets:
+                if name == key:
+                    return self._construct_fieldset(name, options)
+        raise KeyError('FieldSet with key %r not found' % key)
+
+    def _construct_fieldset(self, name, options):
+        fields = SortedDict((field_name, self.form.fields[field_name])
+            for field_name in options['fields'] if field_name in self.form.fields)
+        return FieldSet(self.form, name, fields, options.get('attrs'),
+            options.get('legend'), options.get('description'))
+
+class FieldSet(object):
+    "Iterable FieldSet as a collection of BoundFields."
+    def __init__(self, form, name, fields, attrs=None, legend=None, description=None):
+        self.form = form
+        self.name = name
+        self.fields = fields
+        self.attrs = attrs
+        self.legend = legend
+        self.description = description
+
+    def __iter__(self):
+        for name, field in self.fields.items():
+            yield BoundField(self.form, field, name)
+
+    def __getitem__(self, name):
+        try:
+            field = self.fields[name]
+        except KeyError:
+            raise KeyError('Key %r not found in FieldSet' % name)
+        return BoundField(self.form, field, name)
+
+    def _errors(self):
+        return ErrorDict((k, v) for (k, v) in self.form.errors.items() if k in self.fields)
+    errors = property(_errors)
+
 class BoundField(StrAndUnicode):
     "A Field plus data"
     def __init__(self, form, field, name):
diff --git a/django/forms/models.py b/django/forms/models.py
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -193,6 +193,7 @@
         self.model = getattr(options, 'model', None)
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
+        self.fieldsets = getattr(options, 'fieldsets', None)
         self.widgets = getattr(options, 'widgets', None)
 
 class ModelFormMetaclass(type):
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -322,6 +322,49 @@
 >>> CategoryForm.base_fields.keys()
 ['name']
 
+
+Using 'fieldsets'
+
+>>> class CategoryForm(ModelForm):
+...
+...     class Meta:
+...         model = Category
+...         fieldsets = (
+...             ('main', {'fields': ('slug',)}),
+...             ('other', {'fields': ('name',)}),
+...         )
+
+>>> CategoryForm.base_fields.keys()
+['name', 'slug']
+
+>>> for fieldset in CategoryForm().fieldsets():
+...     print fieldset.name, fieldset.fields.keys()
+main ['slug']
+other ['name']
+
+
+Using 'fields' *and* 'exclude' *and* 'fieldsets'
+
+>>> class CategoryForm(ModelForm):
+...
+...     class Meta:
+...         model = Category
+...         fields = ['name', 'url']
+...         exclude = ['url']
+...         fieldsets = (
+...             ('main', {'fields': ('slug',)}),
+...             ('other', {'fields': ('name',)}),
+...         )
+
+>>> CategoryForm.base_fields.keys()
+['name']
+
+>>> for fieldset in CategoryForm().fieldsets():
+...     print fieldset.name, fieldset.fields.keys()
+main []
+other ['name']
+
+
 Using 'widgets'
 
 >>> class CategoryForm(ModelForm):
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -966,6 +966,142 @@
 <tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
 <tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
 
+Or to specify fieldsets.
+>>> class FieldsetsTestForm(TestForm):
+...    class Meta:
+...        fieldsets = (
+...            ('main', {'fields': ('field3', 'field8', 'field2')}),
+...            ('other', {'fields': ('field5', 'field11', 'field14')}),
+...        )
+>>> p = FieldsetsTestForm(auto_id=False)
+>>> print p
+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
+<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
+<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
+<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
+<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
+>>> print p.has_fieldsets
+True
+>>> for fieldset in p.fieldsets():
+...     print fieldset.name
+...     for field in fieldset:
+...         print field 
+main
+<input type="text" name="field3" />
+<input type="text" name="field8" />
+<input type="text" name="field2" />
+other
+<input type="text" name="field5" />
+<input type="text" name="field11" />
+<input type="text" name="field14" />
+
+Fieldsets and fields in fieldsets can be identified by names.
+>>> print p.fieldsets()['main'].name
+main
+>>> print p.fieldsets()['other']['field5']
+<input type="text" name="field5" />
+
+Each fieldset has attribute errors for current fields.
+>>> for fieldset in p.fieldsets():
+...      fieldset.errors.as_ul()
+u''
+u''
+>>> p = FieldsetsTestForm({'field3': '3', 'field2': '2', 'field5': '5', 'field11': '11'})
+>>> for fieldset in p.fieldsets():
+...      fieldset.errors.as_ul()
+u'<ul class="errorlist"><li>field8<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
+u'<ul class="errorlist"><li>field14<ul class="errorlist"><li>This field is required.</li></ul></li></ul>'
+
+Or to use fields and exlude at once.
+>>> class CombinedTestForm(TestForm):
+...    class Meta:
+...        fields = ('field4', 'field2', 'field11', 'field8')
+...        exclude = ('field14', 'field3', 'field5', 'field8', 'field1')
+...        fieldsets = (
+...            ('main', {'fields': ('field3', 'field8', 'field2')}),
+...            ('other', {'fields': ('field5', 'field11', 'field14')}),
+...        )
+>>> p = CombinedTestForm(auto_id=False)
+>>> print p
+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
+>>> print p.has_fieldsets
+True
+>>> for fieldset in p.fieldsets():
+...     print fieldset.name
+...     for field in fieldset:
+...         print field 
+main
+<input type="text" name="field2" />
+other
+<input type="text" name="field11" />
+
+You can use fieldsets in your general templates also if you do not define any. 
+>>> class OrderedTestForm(TestForm):
+...    class Meta:
+...        fields = ('field4', 'field2', 'field11', 'field8')
+>>> p = OrderedTestForm(auto_id=False)
+>>> print p.has_fieldsets
+False
+>>> for fieldset in p.fieldsets():
+...     print fieldset.name
+...     for field in fieldset:
+...         print field 
+None
+<input type="text" name="field4" />
+<input type="text" name="field2" />
+<input type="text" name="field11" />
+<input type="text" name="field8" />
+
+Fieldsets can have some additional options - attrs, legend and description.
+You can use them in your templates.
+>>> class FieldsetsTestForm(TestForm):
+...    class Meta:
+...        fieldsets = (
+...            ('main', {
+...                'fields': ('field3', 'field8', 'field2'),
+...                'legend': 'Main fields',
+...                'description': 'You should really fill these fields.',
+...            }),
+...            ('other', {
+...                'fields': ('field5', 'field11', 'field14'),
+...                'legend': 'Other fields',
+...                'attrs': {'class': 'other'},
+...            }),
+...        )
+>>> p = FieldsetsTestForm(auto_id=False)
+>>> print p
+<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>
+<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
+<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr>
+<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr>
+<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr>
+<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>
+>>> print p.has_fieldsets
+True
+>>> for fieldset in p.fieldsets():
+...     print 'name:', fieldset.name
+...     print 'attrs:', fieldset.attrs
+...     print 'description:', fieldset.description
+...     print 'legend:', fieldset.legend
+...     for field in fieldset:
+...         print field  
+name: main
+attrs: None
+description: You should really fill these fields.
+legend: Main fields
+<input type="text" name="field3" />
+<input type="text" name="field8" />
+<input type="text" name="field2" />
+name: other
+attrs: {'class': 'other'}
+description: None
+legend: Other fields
+<input type="text" name="field5" />
+<input type="text" name="field11" />
+<input type="text" name="field14" />
+
 Some Field classes have an effect on the HTML attributes of their associated
 Widget. If you set max_length in a CharField and its associated widget is
 either a TextInput or PasswordInput, then the widget's rendered HTML will
