Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py	(revision 5400)
+++ django/db/models/base.py	(working copy)
@@ -317,7 +317,13 @@
 
     def _get_FIELD_display(self, field):
         value = getattr(self, field.attname)
-        return dict(field.choices).get(value, value)
+        newchoices = []
+        for choice in field.choices:
+            if type(choice[1]) == list or type(choice[1]) == tuple:
+                newchoices.extend(choice[1])
+            else:
+                newchoices.append(choice)
+        return dict(newchoices).get(value, value)
 
     def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs):
         op = is_next and '>' or '<'
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 5400)
+++ tests/regressiontests/forms/tests.py	(working copy)
@@ -392,6 +392,33 @@
 <option value="4">4</option>
 </select>
 
+Choices can be nested one level in order to create HTML optgroups:
+>>> w = Select(choices=(('outer1', 'Outer 1'), ('Group 1', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))))
+>>> print w.render('nestchoice', None)
+<select name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+>>> print w.render('nestchoice', 'outer1')
+<select name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+>>> print w.render('nestchoice', 'inner1')
+<select name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1" selected="selected">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+
 # NullBooleanSelect Widget ####################################################
 
 >>> w = NullBooleanSelect()
@@ -533,6 +560,41 @@
 >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')])
 u'<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>'
 
+Choices can be nested one level in order to create HTML optgroups:
+>>> w = SelectMultiple(choices=(('outer1', 'Outer 1'), ('Group 1', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))))
+>>> print w.render('nestchoice', None)
+<select multiple="multiple" name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+>>> print w.render('nestchoice', ['outer1'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+>>> print w.render('nestchoice', ['inner1'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1" selected="selected">Inner 1</option>
+<option value="inner2">Inner 2</option>
+</optgroup>
+</select>
+>>> print w.render('nestchoice', ['outer1', 'inner2'])
+<select multiple="multiple" name="nestchoice">
+<option value="outer1" selected="selected">Outer 1</option>
+<optgroup label="Group 1">
+<option value="inner1">Inner 1</option>
+<option value="inner2" selected="selected">Inner 2</option>
+</optgroup>
+</select>
+
 # RadioSelect Widget ##########################################################
 
 >>> w = RadioSelect()
Index: django/newforms/fields.py
===================================================================
--- django/newforms/fields.py	(revision 5400)
+++ django/newforms/fields.py	(working copy)
@@ -427,7 +427,14 @@
         value = smart_unicode(value)
         if value == u'':
             return value
-        valid_values = set([str(k) for k, v in self.choices])
+        valid_values = []
+        for k, v in self.choices:
+            if type(v) == tuple or type(v) == list:
+                for k2, v2 in v:
+                    valid_values.append(str(k2))
+                continue
+            valid_values.append(str(k))
+        valid_values = set(valid_values)
         if value not in valid_values:
             raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
         return value
@@ -451,7 +458,14 @@
             val = smart_unicode(val)
             new_value.append(val)
         # Validate that each value in the value list is in self.choices.
-        valid_values = set([smart_unicode(k) for k, v in self.choices])
+        valid_values = []
+        for k, v in self.choices:
+            if type(v) == tuple or type(v) == list:
+                for k2, v2 in v:
+                    valid_values.append(smart_unicode(k2))
+                continue
+            valid_values.append(smart_unicode(k))
+        valid_values = set(valid_values)
         for val in new_value:
             if val not in valid_values:
                 raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
Index: django/newforms/widgets.py
===================================================================
--- django/newforms/widgets.py	(revision 5400)
+++ django/newforms/widgets.py	(working copy)
@@ -168,6 +168,14 @@
         output = [u'<select%s>' % flatatt(final_attrs)]
         str_value = smart_unicode(value) # Normalize to string.
         for option_value, option_label in chain(self.choices, choices):
+            if type(option_label) == list or type(option_label) == tuple:
+                output.append(u'<optgroup label="%s">' % escape(smart_unicode(option_value)))
+                for group_option_value, group_option_label in option_label:
+                    group_option_value = smart_unicode(group_option_value)
+                    selected_html = (group_option_value == str_value) and u' selected="selected"' or ''
+                    output.append(u'<option value="%s"%s>%s</option>' % (escape(group_option_value), selected_html, escape(smart_unicode(group_option_label))))
+                output.append(u'</optgroup>')
+                continue
             option_value = smart_unicode(option_value)
             selected_html = (option_value == str_value) and u' selected="selected"' or ''
             output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(smart_unicode(option_label))))
@@ -205,6 +213,14 @@
         output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
         str_values = set([smart_unicode(v) for v in value]) # Normalize to strings.
         for option_value, option_label in chain(self.choices, choices):
+            if type(option_label) == list or type(option_label) == tuple:
+                output.append(u'<optgroup label="%s">' % escape(smart_unicode(option_value)))
+                for group_option_value, group_option_label in option_label:
+                    group_option_value = smart_unicode(group_option_value)
+                    selected_html = (group_option_value in str_values) and ' selected="selected"' or ''
+                    output.append(u'<option value="%s"%s>%s</option>' % (escape(group_option_value), selected_html, escape(smart_unicode(group_option_label))))
+                output.append(u'</optgroup>')
+                continue
             option_value = smart_unicode(option_value)
             selected_html = (option_value in str_values) and ' selected="selected"' or ''
             output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(smart_unicode(option_label))))
Index: docs/model-api.txt
===================================================================
--- docs/model-api.txt	(revision 5400)
+++ docs/model-api.txt	(working copy)
@@ -537,6 +537,22 @@
     class Foo(models.Model):
         gender = models.CharField(maxlength=1, choices=GENDER_CHOICES)
 
+The choices list may also be nested one level::
+
+    MEDIA_CHOICES = (
+        ('Audio', (
+                ('vinyl', 'Vinyl'),
+                ('cd', 'CD'),
+            )
+        ),
+        ('Video', (
+                ('vhs', 'VHS Tape'),
+                ('dvd', 'DVD'),
+            )
+        ),
+        ('unknown', 'Unknown'),
+    )
+
 For each model field that has ``choices`` set, Django will add a method to
 retrieve the human-readable name for the field's current value. See
 `get_FOO_display`_ in the database API documentation.
