Index: django/newforms/fields.py
===================================================================
--- django/newforms/fields.py	(revision 5371)
+++ 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 5371)
+++ 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: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 5371)
+++ 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()
