Index: django/template/defaulttags.py
===================================================================
--- django/template/defaulttags.py	(revision 5377)
+++ django/template/defaulttags.py	(working copy)
@@ -61,8 +61,8 @@
         return ''
 
 class ForNode(Node):
-    def __init__(self, loopvar, sequence, reversed, nodelist_loop):
-        self.loopvar, self.sequence = loopvar, sequence
+    def __init__(self, loopvars, sequence, reversed, nodelist_loop):
+        self.loopvars, self.sequence = loopvars, sequence
         self.reversed = reversed
         self.nodelist_loop = nodelist_loop
 
@@ -72,7 +72,7 @@
         else:
             reversed = ''
         return "<For Node: for %s in %s, tail_len: %d%s>" % \
-            (self.loopvar, self.sequence, len(self.nodelist_loop), reversed)
+            (', '.join( self.loopvars ), self.sequence, len(self.nodelist_loop), reversed)
 
     def __iter__(self):
         for node in self.nodelist_loop:
@@ -107,6 +107,7 @@
                 for index in range(len(data)-1, -1, -1):
                     yield data[index]
             values = reverse(values)
+        unpack = len(loopvars) > 1
         for i, item in enumerate(values):
             context['forloop'] = {
                 # shortcuts for current loop iteration number
@@ -120,9 +121,20 @@
                 'last': (i == len_values - 1),
                 'parentloop': parentloop,
             }
-            context[self.loopvar] = item
+            if unpack:
+                # If multiple loop variables, unpack the item to them.
+                context.update(dict(zip(self.loopvars, item)))
+            else:
+                context[self.loopvars[0]] = item
             for node in self.nodelist_loop:
                 nodelist.append(node.render(context))
+            if unpack:
+                # The loop variables were pushed on to the context so pop them
+                # off again. This is necessary because the tag lets the length
+                # of loopvars differ to the length of each set of items and we
+                # don't want to leave any vars from this previous loop on the
+                # context.
+                context.pop()
         context.pop()
         return nodelist.render(context)
 
@@ -486,7 +498,7 @@
     nodelist = parser.parse(('endfilter',))
     parser.delete_first_token()
     return FilterNode(filter_expr, nodelist)
-filter = register.tag("filter", do_filter)
+do_filter = register.tag("filter", do_filter)
 
 #@register.tag
 def firstof(parser, token):
@@ -530,8 +542,14 @@
         {% endfor %}
         </ul>
 
-    You can also loop over a list in reverse by using
+    You can loop over a list in reverse by using
     ``{% for obj in list reversed %}``.
+    
+    You can also unpack multiple values from a two-dimensional array::
+    
+        {% for key,value in dict.items %}
+            {{ key }}: {{ value }}
+        {% endfor %}
 
     The for loop sets a number of variables available within the loop:
 
@@ -552,18 +570,23 @@
 
     """
     bits = token.contents.split()
-    if len(bits) == 5 and bits[4] != 'reversed':
-        raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents
-    if len(bits) not in (4, 5):
-        raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents
-    if bits[2] != 'in':
-        raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents
-    loopvar = bits[1]
-    sequence = parser.compile_filter(bits[3])
-    reversed = (len(bits) == 5)
+    if len(bits) < 4:
+        raise TemplateSyntaxError, "'for' statements should have at least four words: %s" % token.contents
+
+    reversed = bits[-1] == 'reversed'
+    in_index = reversed and -3 or -2
+    if bits[in_index] != 'in':
+        raise TemplateSyntaxError, "'for' statements should use the format 'for x in y': %s" % token.contents
+
+    loopvars = ' '.join(bits[1:in_index]).replace(', ', ',').split(',')
+    for var in loopvars:
+        if not var or ' ' in var:
+            raise TemplateSyntaxError, "'for' tag received an invalid argument: %s" % token.contents
+
+    sequence = parser.compile_filter(bits[in_index+1])
     nodelist_loop = parser.parse(('endfor',))
     parser.delete_first_token()
-    return ForNode(loopvar, sequence, reversed, nodelist_loop)
+    return ForNode(loopvars, sequence, reversed, nodelist_loop)
 do_for = register.tag("for", do_for)
 
 def do_ifequal(parser, token, negate):
Index: docs/templates.txt
===================================================================
--- docs/templates.txt	(revision 5377)
+++ docs/templates.txt	(working copy)
@@ -444,7 +444,7 @@
 ~~~
 
 Loop over each item in an array.  For example, to display a list of athletes
-given ``athlete_list``::
+provided in ``athlete_list``::
 
     <ul>
     {% for athlete in athlete_list %}
@@ -452,8 +452,15 @@
     {% endfor %}
     </ul>
 
-You can also loop over a list in reverse by using ``{% for obj in list reversed %}``.
+You can loop over a list in reverse by using ``{% for obj in list reversed %}``.
 
+For advanced use, you can unpack multiple values from a list of fixed length
+lists. For example, to display the keys and values of a Python dictionary::
+
+    {% for key, value in dict.iteritems %}
+        {{ key }}: {{ value }}
+    {% endfor %}
+
 The for loop sets a number of variables available within the loop:
 
     ==========================  ================================================
Index: tests/regressiontests/templates/tests.py
===================================================================
--- tests/regressiontests/templates/tests.py	(revision 5377)
+++ tests/regressiontests/templates/tests.py	(working copy)
@@ -289,6 +289,17 @@
             'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"),
             'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"),
             'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"),
+            'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
+            'for-tag-unpack02': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
+            'for-tag-unpack03': ("{% for key value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
+            'for-tag-unpack04': ("{% for key,,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
+            'for-tag-unpack05': ("{% for key,value, in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError),
+            # Ensure that a single loopvar doesn't truncate the list in val.
+            'for-tag-unpack06': ("{% for val in items %}{{ val.0 }}:{{ val.1 }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
+            # Otherwise, silently truncate if the length of loopvars differs to the length of each set of items.
+            'for-tag-unpack07': ("{% for x,y in items %}{{ x }}:{{ y }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'orange'))}, "one:1/two:2/"),
+            'for-tag-unpack08': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, ("one:1,/two:2,/", "one:1,INVALID/two:2,INVALID/")),
+            'for-tag-unpack09': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2))}, ("one:1,carrot/two:2,/", "one:1,carrot/two:2,INVALID/")),
 
             ### IF TAG ################################################################
             'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),
