Index: django/contrib/localflavor/us/us_states.py
===================================================================
--- django/contrib/localflavor/us/us_states.py	(revision 11468)
+++ django/contrib/localflavor/us/us_states.py	(working copy)
@@ -232,3 +232,254 @@
     'wyo': 'WY',
     'wyoming': 'WY',
 }
+
+# These choices should be kept in sync with:
+#   http://www.usps.com/ncsc/lookups/abbreviations.html
+US_POSTAL_CODE_CHOICES = (
+    ('AL', 'Alabama'),
+    ('AK', 'Alaska'),
+    ('AS', 'American Samoa'),
+    ('AZ', 'Arizona'),
+    ('AR', 'Arkansas'),
+    ('AA', 'Armed Forces Americas'),
+    ('AE', 'Armed Forces Europe'),
+    ('AP', 'Armed Forces Pacific'),
+    ('CA', 'California'),
+    ('CO', 'Colorado'),
+    ('CT', 'Connecticut'),
+    ('DE', 'Delaware'),
+    ('DC', 'District of Columbia'),
+    ('FM', 'Federated States of Micronesia'),
+    ('FL', 'Florida'),
+    ('GA', 'Georgia'),
+    ('GU', 'Guam'),
+    ('HI', 'Hawaii'),
+    ('ID', 'Idaho'),
+    ('IL', 'Illinois'),
+    ('IN', 'Indiana'),
+    ('IA', 'Iowa'),
+    ('KS', 'Kansas'),
+    ('KY', 'Kentucky'),
+    ('LA', 'Louisiana'),
+    ('ME', 'Maine'),
+    ('MH', 'Marshall Islands'),
+    ('MD', 'Maryland'),
+    ('MA', 'Massachusetts'),
+    ('MI', 'Michigan'),
+    ('MN', 'Minnesota'),
+    ('MS', 'Mississippi'),
+    ('MO', 'Missouri'),
+    ('MT', 'Montana'),
+    ('NE', 'Nebraska'),
+    ('NV', 'Nevada'),
+    ('NH', 'New Hampshire'),
+    ('NJ', 'New Jersey'),
+    ('NM', 'New Mexico'),
+    ('NY', 'New York'),
+    ('NC', 'North Carolina'),
+    ('ND', 'North Dakota'),
+    ('MP', 'Northern Mariana Islands'),
+    ('OH', 'Ohio'),
+    ('OK', 'Oklahoma'),
+    ('OR', 'Oregon'),
+    ('PW', 'Palau'),
+    ('PA', 'Pennsylvania'),
+    ('PR', 'Puerto Rico'),
+    ('RI', 'Rhode Island'),
+    ('SC', 'South Carolina'),
+    ('SD', 'South Dakota'),
+    ('TN', 'Tennessee'),
+    ('TX', 'Texas'),
+    ('UT', 'Utah'),
+    ('VT', 'Vermont'),
+    ('VI', 'Virgin Islands'),
+    ('VA', 'Virginia'),
+    ('WA', 'Washington'),
+    ('WV', 'West Virginia'),
+    ('WI', 'Wisconsin'),
+    ('WY', 'Wyoming'),
+)
+
+US_POSTAL_CODES_NORMALIZED = {
+    'aa': 'AA',
+    'ae': 'AE',
+    'ak': 'AK',
+    'al': 'AL',
+    'ala': 'AL',
+    'alabama': 'AL',
+    'alaska': 'AK',
+    'american samao': 'AS',
+    'american samoa': 'AS',
+    'ap': 'AP',
+    'ar': 'AR',
+    'ariz': 'AZ',
+    'arizona': 'AZ',
+    'ark': 'AR',
+    'arkansas': 'AR',
+    'armed forces africa': 'AE',
+    'armed forces americas': 'AA',
+    'armed forces europe': 'AE',
+    'armed forces pacific': 'AP',
+    'armed forces canada': 'AE',
+    'armed forces middle east': 'AE',
+    'as': 'AS',
+    'az': 'AZ',
+    'ca': 'CA',
+    'calf': 'CA',
+    'calif': 'CA',
+    'california': 'CA',
+    'co': 'CO',
+    'colo': 'CO',
+    'colorado': 'CO',
+    'conn': 'CT',
+    'connecticut': 'CT',
+    'ct': 'CT',
+    'dc': 'DC',
+    'de': 'DE',
+    'del': 'DE',
+    'delaware': 'DE',
+    'deleware': 'DE',
+    'district of columbia': 'DC',
+    'federated states of micronesia': 'FM',
+    'fl': 'FL',
+    'fla': 'FL',
+    'florida': 'FL',
+    'fm': 'FM',
+    'ga': 'GA',
+    'georgia': 'GA',
+    'gu': 'GU',
+    'guam': 'GU',
+    'hawaii': 'HI',
+    'hi': 'HI',
+    'ia': 'IA',
+    'id': 'ID',
+    'idaho': 'ID',
+    'il': 'IL',
+    'ill': 'IL',
+    'illinois': 'IL',
+    'in': 'IN',
+    'ind': 'IN',
+    'indiana': 'IN',
+    'iowa': 'IA',
+    'kan': 'KS',
+    'kans': 'KS',
+    'kansas': 'KS',
+    'kentucky': 'KY',
+    'ks': 'KS',
+    'ky': 'KY',
+    'la': 'LA',
+    'louisiana': 'LA',
+    'ma': 'MA',
+    'maine': 'ME',
+    'marianas islands': 'MP',
+    'marianas islands of the pacific': 'MP',
+    'marinas islands of the pacific': 'MP',
+    'marshall islands': 'MH',
+    'maryland': 'MD',
+    'mass': 'MA',
+    'massachusetts': 'MA',
+    'massachussetts': 'MA',
+    'md': 'MD',
+    'me': 'ME',
+    'mh': 'MH',
+    'mi': 'MI',
+    'mich': 'MI',
+    'michigan': 'MI',
+    'micronesia': 'FM',
+    'minn': 'MN',
+    'minnesota': 'MN',
+    'miss': 'MS',
+    'mississippi': 'MS',
+    'missouri': 'MO',
+    'mn': 'MN',
+    'mo': 'MO',
+    'mont': 'MT',
+    'montana': 'MT',
+    'mp': 'MP',
+    'ms': 'MS',
+    'mt': 'MT',
+    'n d': 'ND',
+    'n dak': 'ND',
+    'n h': 'NH',
+    'n j': 'NJ',
+    'n m': 'NM',
+    'n mex': 'NM',
+    'nc': 'NC',
+    'nd': 'ND',
+    'ne': 'NE',
+    'neb': 'NE',
+    'nebr': 'NE',
+    'nebraska': 'NE',
+    'nev': 'NV',
+    'nevada': 'NV',
+    'new hampshire': 'NH',
+    'new jersey': 'NJ',
+    'new mexico': 'NM',
+    'new york': 'NY',
+    'nh': 'NH',
+    'nj': 'NJ',
+    'nm': 'NM',
+    'nmex': 'NM',
+    'north carolina': 'NC',
+    'north dakota': 'ND',
+    'northern mariana islands': 'MP',
+    'nv': 'NV',
+    'ny': 'NY',
+    'oh': 'OH',
+    'ohio': 'OH',
+    'ok': 'OK',
+    'okla': 'OK',
+    'oklahoma': 'OK',
+    'or': 'OR',
+    'ore': 'OR',
+    'oreg': 'OR',
+    'oregon': 'OR',
+    'pa': 'PA',
+    'palau': 'PW',
+    'penn': 'PA',
+    'pennsylvania': 'PA',
+    'pr': 'PR',
+    'puerto rico': 'PR',
+    'pw': 'PW',
+    'rhode island': 'RI',
+    'ri': 'RI',
+    's dak': 'SD',
+    'sc': 'SC',
+    'sd': 'SD',
+    'sdak': 'SD',
+    'south carolina': 'SC',
+    'south dakota': 'SD',
+    'tenn': 'TN',
+    'tennessee': 'TN',
+    'territory of hawaii': 'HI',
+    'tex': 'TX',
+    'texas': 'TX',
+    'tn': 'TN',
+    'tx': 'TX',
+    'us virgin islands': 'VI',
+    'usvi': 'VI',
+    'ut': 'UT',
+    'utah': 'UT',
+    'va': 'VA',
+    'vermont': 'VT',
+    'vi': 'VI',
+    'viginia': 'VA',
+    'virgin islands': 'VI',
+    'virgina': 'VA',
+    'virginia': 'VA',
+    'vt': 'VT',
+    'w va': 'WV',
+    'wa': 'WA',
+    'wash': 'WA',
+    'washington': 'WA',
+    'west virginia': 'WV',
+    'wi': 'WI',
+    'wis': 'WI',
+    'wisc': 'WI',
+    'wisconsin': 'WI',
+    'wv': 'WV',
+    'wva': 'WV',
+    'wy': 'WY',
+    'wyo': 'WY',
+    'wyoming': 'WY',
+} 
Index: django/contrib/localflavor/us/models.py
===================================================================
--- django/contrib/localflavor/us/models.py	(revision 11468)
+++ django/contrib/localflavor/us/models.py	(working copy)
@@ -17,6 +17,25 @@
         defaults.update(kwargs) 
         return super(USStateField, self).formfield(**defaults)
 
+class USPostalCodeField(Field): 
+    """
+    State field supporting valid USPS two-letter codes.
+    """
+    def get_internal_type(self): 
+        return "USPostalCodeField" 
+        
+    def db_type(self):
+        if settings.DATABASE_ENGINE == 'oracle':
+            return 'CHAR(2)'
+        else:
+            return 'varchar(2)'
+    
+    def formfield(self, **kwargs): 
+        from django.contrib.localflavor.us.forms import USPostalCodeSelect 
+        defaults = {'widget': USPostalCodeSelect} 
+        defaults.update(kwargs) 
+        return super(USPostalCodeField, self).formfield(**defaults)
+
 class PhoneNumberField(Field):
     def get_internal_type(self):
         return "PhoneNumberField"
Index: django/contrib/localflavor/us/forms.py
===================================================================
--- django/contrib/localflavor/us/forms.py	(revision 11468)
+++ django/contrib/localflavor/us/forms.py	(working copy)
@@ -110,3 +110,37 @@
     def __init__(self, attrs=None):
         from us_states import STATE_CHOICES
         super(USStateSelect, self).__init__(attrs, choices=STATE_CHOICES)
+
+class USPostalCodeField(Field):
+    """
+    A form field that validates its input is a US Postal Code state name or abbreviation.
+    It normalizes the input to the standard two-leter postal service
+    abbreviation for the given state.
+    """
+    default_error_messages = {
+        'invalid': u'Enter a U.S. state or territory.',
+    }
+
+    def clean(self, value):
+        from us_states import US_POSTAL_CODES_NORMALIZED
+        super(USPostalCodeField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        try:
+            value = value.strip().lower()
+        except AttributeError:
+            pass
+        else:
+            try:
+                return US_POSTAL_CODES_NORMALIZED[value.strip().lower()].decode('ascii')
+            except KeyError:
+                pass
+        raise ValidationError(self.error_messages['invalid'])
+
+class USPostalCodeSelect(Select):
+    """
+    A Select widget that uses a list of USPS postal codes as its choices.
+    """
+    def __init__(self, attrs=None):
+        from us_states import US_POSTAL_CODE_CHOICES
+        super(USPostalCodeSelect, self).__init__(attrs, choices=US_POSTAL_CODE_CHOICES)
Index: tests/regressiontests/forms/localflavor/us.py
===================================================================
--- tests/regressiontests/forms/localflavor/us.py	(revision 11468)
+++ tests/regressiontests/forms/localflavor/us.py	(working copy)
@@ -243,6 +243,123 @@
 <option value="WY">Wyoming</option>
 </select>
 
+# USPostalCodeField ################################################################
+
+USPostalCodeField validates that the data is either an abbreviation or name of a
+USPS postal code.
+>>> from django.contrib.localflavor.us.forms import USPostalCodeField
+>>> f = USPostalCodeField()
+>>> f.clean('mo')
+u'MO'
+>>> f.clean('MO')
+u'MO'
+>>> f.clean('mIssouri')
+u'MO'
+>>> f.clean('  mIssouri ')
+u'MO'
+>>> f.clean(63303)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a U.S. state or territory.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = USPostalCodeField(required=False)
+>>> f.clean('mo')
+u'MO'
+>>> f.clean('MO')
+u'MO'
+>>> f.clean('mIssouri')
+u'MO'
+>>> f.clean('  mIssouri ')
+u'MO'
+>>> f.clean(63303)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a U.S. state or territory.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# USPostalCodeSelect ###############################################################
+
+USPostalCodeSelect is a Select widget that uses a list of USPS postal codes
+as its choices.
+>>> from django.contrib.localflavor.us.forms import USPostalCodeSelect
+>>> w = USPostalCodeSelect()
+>>> print w.render('state', 'MO')
+<select name="state">
+<option value="AL">Alabama</option>
+<option value="AK">Alaska</option>
+<option value="AS">American Samoa</option>
+<option value="AZ">Arizona</option>
+<option value="AR">Arkansas</option>
+<option value="AA">Armed Forces Americas</option>
+<option value="AE">Armed Forces Europe</option>
+<option value="AP">Armed Forces Pacific</option>
+<option value="CA">California</option>
+<option value="CO">Colorado</option>
+<option value="CT">Connecticut</option>
+<option value="DE">Delaware</option>
+<option value="DC">District of Columbia</option>
+<option value="FM">Federated States of Micronesia</option>
+<option value="FL">Florida</option>
+<option value="GA">Georgia</option>
+<option value="GU">Guam</option>
+<option value="HI">Hawaii</option>
+<option value="ID">Idaho</option>
+<option value="IL">Illinois</option>
+<option value="IN">Indiana</option>
+<option value="IA">Iowa</option>
+<option value="KS">Kansas</option>
+<option value="KY">Kentucky</option>
+<option value="LA">Louisiana</option>
+<option value="ME">Maine</option>
+<option value="MH">Marshall Islands</option>
+<option value="MD">Maryland</option>
+<option value="MA">Massachusetts</option>
+<option value="MI">Michigan</option>
+<option value="MN">Minnesota</option>
+<option value="MS">Mississippi</option>
+<option value="MO" selected="selected">Missouri</option>
+<option value="MT">Montana</option>
+<option value="NE">Nebraska</option>
+<option value="NV">Nevada</option>
+<option value="NH">New Hampshire</option>
+<option value="NJ">New Jersey</option>
+<option value="NM">New Mexico</option>
+<option value="NY">New York</option>
+<option value="NC">North Carolina</option>
+<option value="ND">North Dakota</option>
+<option value="MP">Northern Mariana Islands</option>
+<option value="OH">Ohio</option>
+<option value="OK">Oklahoma</option>
+<option value="OR">Oregon</option>
+<option value="PW">Palau</option>
+<option value="PA">Pennsylvania</option>
+<option value="PR">Puerto Rico</option>
+<option value="RI">Rhode Island</option>
+<option value="SC">South Carolina</option>
+<option value="SD">South Dakota</option>
+<option value="TN">Tennessee</option>
+<option value="TX">Texas</option>
+<option value="UT">Utah</option>
+<option value="VT">Vermont</option>
+<option value="VI">Virgin Islands</option>
+<option value="VA">Virginia</option>
+<option value="WA">Washington</option>
+<option value="WV">West Virginia</option>
+<option value="WI">Wisconsin</option>
+<option value="WY">Wyoming</option>
+</select>
+
 # USSocialSecurityNumberField #################################################
 >>> from django.contrib.localflavor.us.forms import USSocialSecurityNumberField
 >>> f = USSocialSecurityNumberField()
Index: tests/regressiontests/serializers_regress/tests.py
===================================================================
--- tests/regressiontests/serializers_regress/tests.py	(revision 11468)
+++ tests/regressiontests/serializers_regress/tests.py	(working copy)
@@ -220,6 +220,9 @@
     (data_obj, 180, USStateData, "MA"),
     (data_obj, 181, USStateData, None),
     (data_obj, 182, USStateData, ""),
+    (data_obj, 183, USPostalCodeData, "MO"),
+    (data_obj, 184, USPostalCodeData, None),
+    (data_obj, 185, USPostalCodeData, ""),
     (data_obj, 190, XMLData, "<foo></foo>"),
     (data_obj, 191, XMLData, None),
     (data_obj, 192, XMLData, ""),
@@ -313,6 +316,7 @@
 # The end."""),
 #    (pk_obj, 770, TimePKData, datetime.time(10,42,37)),
     (pk_obj, 780, USStatePKData, "MA"),
+    (pk_obj, 781, USPostalCodePKData, "MO"),
 #     (pk_obj, 790, XMLPKData, "<foo></foo>"),
 
     (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)),
Index: tests/regressiontests/serializers_regress/models.py
===================================================================
--- tests/regressiontests/serializers_regress/models.py	(revision 11468)
+++ tests/regressiontests/serializers_regress/models.py	(working copy)
@@ -8,7 +8,7 @@
 from django.db import models
 from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.localflavor.us.models import USStateField, PhoneNumberField
+from django.contrib.localflavor.us.models import USStateField, USPostalCodeField, PhoneNumberField
 
 # The following classes are for testing basic data
 # marshalling, including NULL values, where allowed.
@@ -76,6 +76,9 @@
 class USStateData(models.Model):
     data = USStateField(null=True)
 
+class USPostalCodeData(models.Model):
+    data = USPostalCodeField(null=True)
+
 class XMLData(models.Model):
     data = models.XMLField(null=True)
 
@@ -212,6 +215,9 @@
 class USStatePKData(models.Model):
     data = USStateField(primary_key=True)
 
+class USPostalCodePKData(models.Model):
+    data = USPostalCodeField(primary_key=True)
+
 # class XMLPKData(models.Model):
 #     data = models.XMLField(primary_key=True)
