diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index d0cc657c4a..b705eac512 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -380,6 +380,9 @@ class Field(object):
             return self._choices
     choices = property(_get_choices)
 
+    def save_form_data(self, instance, data):
+        setattr(instance, self.name, data)
+        
     def formfield(self, form_class=forms.CharField, **kwargs):
         "Returns a django.newforms.Field instance for this database Field."
         defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
@@ -696,6 +699,13 @@ class FileField(Field):
         self.upload_to = upload_to
         Field.__init__(self, verbose_name, name, **kwargs)
 
+    def get_db_prep_save(self, value):
+        "Returns field's value prepared for saving into a database."
+        # Need to convert UploadedFile objects provided via a form to unicode for database insertion
+        if value is None:
+            return None
+        return unicode(value)
+
     def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
         field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
         if not self.blank:
@@ -772,6 +782,19 @@ class FileField(Field):
         f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
         return os.path.normpath(f)
 
+    def save_form_data(self, instance, data):
+        if data:
+            getattr(instance, "save_%s_file" % self.name)(os.path.join(self.upload_to, data.filename), data.content, save=False)
+        
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.FileField}
+        # If a file has been provided previously, then the form doesn't require 
+        # that a new file is provided this time.
+        if 'initial' in kwargs:
+            defaults['required'] = False
+        defaults.update(kwargs)
+        return super(FileField, self).formfield(**defaults)
+
 class FilePathField(Field):
     def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
         self.path, self.match, self.recursive = path, match, recursive
@@ -820,6 +843,10 @@ class ImageField(FileField):
                 setattr(new_object, self.height_field, getattr(original_object, self.height_field))
             new_object.save()
 
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.ImageField}
+        return super(ImageField, self).formfield(**defaults)
+
 class IntegerField(Field):
     empty_strings_allowed = False
     def get_manipulator_field_objs(self):
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index c4284f04b1..d3603b0016 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -756,6 +756,9 @@ class ManyToManyField(RelatedField, Field):
         "Returns the value of this field in the given model instance."
         return getattr(obj, self.attname).all()
 
+    def save_form_data(self, instance, data):
+        setattr(instance, self.attname, data)
+        
     def formfield(self, **kwargs):
         defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()}
         defaults.update(kwargs)
diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py
index 724dcd9b50..96b1c7244d 100644
--- a/django/newforms/extras/widgets.py
+++ b/django/newforms/extras/widgets.py
@@ -53,7 +53,7 @@ class SelectDateWidget(Widget):
 
         return u'\n'.join(output)
 
-    def value_from_datadict(self, data, name):
+    def value_from_datadict(self, data, files, name):
         y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
         if y and m and d:
             return '%s-%s-%s' % (y, m, d)
diff --git a/django/newforms/fields.py b/django/newforms/fields.py
index 79e90da5fb..e9f50ad4ec 100644
--- a/django/newforms/fields.py
+++ b/django/newforms/fields.py
@@ -7,10 +7,10 @@ import re
 import time
 
 from django.utils.translation import ugettext
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import StrAndUnicode, smart_unicode
 
 from util import ErrorList, ValidationError
-from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
 
 try:
     from decimal import Decimal, DecimalException
@@ -22,7 +22,7 @@ __all__ = (
     'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
     'DEFAULT_TIME_INPUT_FORMATS', 'TimeField',
     'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
-    'RegexField', 'EmailField', 'URLField', 'BooleanField',
+    'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField',
     'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
     'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
     'SplitDateTimeField',
@@ -348,6 +348,55 @@ except ImportError:
     # It's OK if Django settings aren't configured.
     URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
 
+class UploadedFile(StrAndUnicode):
+    "A wrapper for files uploaded in a FileField"
+    def __init__(self, filename, content):
+        self.filename = filename
+        self.content = content
+        
+    def __unicode__(self):
+        """
+        The unicode representation is the filename, so that the pre-database-insertion
+        logic can use UploadedFile objects
+        """
+        return self.filename
+
+class FileField(Field):
+    widget = FileInput
+    def __init__(self, *args, **kwargs):
+        super(FileField, self).__init__(*args, **kwargs)
+
+    def clean(self, data):
+        super(FileField, self).clean(data)
+        if not self.required and data in EMPTY_VALUES:
+            return None
+        try:
+            f = UploadedFile(data['filename'], data['content'])
+        except TypeError:
+            raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form."))
+        except KeyError:
+            raise ValidationError(ugettext(u"No file was submitted."))
+        if not f.content:
+            raise ValidationError(ugettext(u"The submitted file is empty."))
+        return f
+
+class ImageField(FileField):
+    def clean(self, data):
+        """
+        Checks that the file-upload field data contains a valid image (GIF, JPG,
+        PNG, possibly others -- whatever the Python Imaging Library supports).
+        """
+        f = super(ImageField, self).clean(data)
+        if f is None:
+            return None
+        from PIL import Image
+        from cStringIO import StringIO
+        try:
+            Image.open(StringIO(f.content))
+        except IOError: # Python Imaging Library doesn't recognize it as an image
+            raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."))
+        return f
+        
 class URLField(RegexField):
     def __init__(self, max_length=None, min_length=None, verify_exists=False,
             validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index 5da85a69c4..906978c86f 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -57,9 +57,10 @@ class BaseForm(StrAndUnicode):
     # class is different than Form. See the comments by the Form class for more
     # information. Any improvements to the form API should be made to *this*
     # class, not to the Form class.
-    def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
-        self.is_bound = data is not None
+    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
+        self.is_bound = data is not None or files is not None
         self.data = data or {}
+        self.files = files or {}
         self.auto_id = auto_id
         self.prefix = prefix
         self.initial = initial or {}
@@ -88,7 +89,7 @@ class BaseForm(StrAndUnicode):
         return BoundField(self, field, name)
 
     def _get_errors(self):
-        "Returns an ErrorDict for self.data"
+        "Returns an ErrorDict for the data provided for the form"
         if self._errors is None:
             self.full_clean()
         return self._errors
@@ -179,10 +180,10 @@ class BaseForm(StrAndUnicode):
             return
         self.cleaned_data = {}
         for name, field in self.fields.items():
-            # value_from_datadict() gets the data from the dictionary.
+            # value_from_datadict() gets the data from the data dictionaries.
             # Each widget type knows how to retrieve its own data, because some
             # widgets split data over several HTML fields.
-            value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
+            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
             try:
                 value = field.clean(value)
                 self.cleaned_data[name] = value
@@ -283,7 +284,7 @@ class BoundField(StrAndUnicode):
         """
         Returns the data for this BoundField, or None if it wasn't given.
         """
-        return self.field.widget.value_from_datadict(self.form.data, self.html_name)
+        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
     data = property(_data)
 
     def label_tag(self, contents=None, attrs=None):
diff --git a/django/newforms/models.py b/django/newforms/models.py
index 7a86e30b1d..247a0eea6b 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -34,7 +34,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True
             continue
         if fields and f.name not in fields:
             continue
-        setattr(instance, f.name, cleaned_data[f.name])
+        f.save_form_data(instance, cleaned_data[f.name])        
     # Wrap up the saving of m2m data as a function
     def save_m2m():
         opts = instance.__class__._meta
@@ -43,7 +43,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True
             if fields and f.name not in fields:
                 continue
             if f.name in cleaned_data:
-                setattr(instance, f.attname, cleaned_data[f.name])
+                f.save_form_data(instance, cleaned_data[f.name])
     if commit:
         # If we are committing, save the instance and the m2m data immediately
         instance.save()
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index e9b9b55470..f985124389 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -47,7 +47,7 @@ class Widget(object):
             attrs.update(extra_attrs)
         return attrs
 
-    def value_from_datadict(self, data, name):
+    def value_from_datadict(self, data, files, name):
         """
         Given a dictionary of data and this widget's name, returns the value
         of this widget. Returns None if it's not provided.
@@ -113,7 +113,7 @@ class MultipleHiddenInput(HiddenInput):
         final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
         return u'\n'.join([(u'<input%s />' % flatatt(dict(value=force_unicode(v), **final_attrs))) for v in value])
 
-    def value_from_datadict(self, data, name):
+    def value_from_datadict(self, data, files, name):
         if isinstance(data, MultiValueDict):
             return data.getlist(name)
         return data.get(name, None)
@@ -121,6 +121,13 @@ class MultipleHiddenInput(HiddenInput):
 class FileInput(Input):
     input_type = 'file'
 
+    def render(self, name, value, attrs=None):
+        return super(FileInput, self).render(name, None, attrs=attrs)
+        
+    def value_from_datadict(self, data, files, name):
+        "File widgets take data from FILES, not POST"
+        return files.get(name, None)
+
 class Textarea(Widget):
     def __init__(self, attrs=None):
         # The 'rows' and 'cols' attributes are required for HTML correctness.
@@ -188,7 +195,7 @@ class NullBooleanSelect(Select):
             value = u'1'
         return super(NullBooleanSelect, self).render(name, value, attrs, choices)
 
-    def value_from_datadict(self, data, name):
+    def value_from_datadict(self, data, files, name):
         value = data.get(name, None)
         return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
 
@@ -210,7 +217,7 @@ class SelectMultiple(Widget):
         output.append(u'</select>')
         return u'\n'.join(output)
 
-    def value_from_datadict(self, data, name):
+    def value_from_datadict(self, data, files, name):
         if isinstance(data, MultiValueDict):
             return data.getlist(name)
         return data.get(name, None)
@@ -377,8 +384,8 @@ class MultiWidget(Widget):
         return id_
     id_for_label = classmethod(id_for_label)
 
-    def value_from_datadict(self, data, name):
-        return [widget.value_from_datadict(data, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+    def value_from_datadict(self, data, files, name):
+        return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
 
     def format_output(self, rendered_widgets):
         """
diff --git a/docs/newforms.txt b/docs/newforms.txt
index dae145434b..5d1da96128 100644
--- a/docs/newforms.txt
+++ b/docs/newforms.txt
@@ -710,6 +710,47 @@ For example::
     </ul>
     </form>
 
+Binding uploaded files to a form
+--------------------------------
+
+Dealing with forms that have ``FileField`` and ``ImageField`` fields
+is a little more complicated than a normal form.
+
+Firstly, in order to upload files, you'll need to make sure that your
+``<form>`` element correctly defines the ``enctype`` as
+``"multipart/form-data"``::
+
+  <form enctype="multipart/form-data" method="post" action="/foo/">
+
+Secondly, when you use the form, you need to bind the file data. File
+data is handled separately to normal form data, so when your form
+contains a ``FileField`` and ``ImageField``, you will need to specify
+a second argument when you bind your form. So if we extend our
+ContactForm to include an ``ImageField`` called ``mugshot``, we
+need to bind the file data containing the mugshot image::
+
+    # Bound form with an image field
+    >>> data = {'subject': 'hello',
+    ...         'message': 'Hi there',
+    ...         'sender': 'foo@example.com',
+    ...         'cc_myself': True}
+    >>> file_data = {'mugshot': {'filename':'face.jpg'
+    ...                          'content': <file data>}}
+    >>> f = ContactFormWithMugshot(data, file_data)
+
+In practice, you will usually specify ``request.FILES`` as the source
+of file data (just like you use ``request.POST`` as the source of
+form data)::
+
+    # Bound form with an image field, data from the request
+    >>> f = ContactFormWithMugshot(request.POST, request.FILES)
+
+Constructing an unbound form is the same as always -- just omit both
+form data *and* file data:
+
+    # Unbound form with a image field
+    >>> f = ContactFormWithMugshot()
+
 Subclassing forms
 -----------------
 
@@ -1099,6 +1140,50 @@ Has two optional arguments for validation, ``max_length`` and ``min_length``.
 If provided, these arguments ensure that the string is at most or at least the
 given length.
 
+``FileField``
+~~~~~~~~~~~~~
+
+    * Default widget: ``FileInput``
+    * Empty value: ``None``
+    * Normalizes to: An ``UploadedFile`` object that wraps the file content
+      and file name into a single object.
+    * Validates that non-empty file data has been bound to the form.
+
+An ``UploadedFile`` object has two attributes:
+
+    ======================  =====================================================
+    Argument                Description
+    ======================  =====================================================
+    ``filename``            The name of the file, provided by the uploading
+                            client.
+    ``content``             The array of bytes comprising the file content.
+    ======================  =====================================================
+
+The string representation of an ``UploadedFile`` is the same as the filename
+attribute.
+
+When you use a ``FileField`` on a form, you must also remember to
+`bind the file data to the form`_.
+
+.. _`bind the file data to the form`: `Binding uploaded files to a form`_
+
+``ImageField``
+~~~~~~~~~~~~~~
+
+    * Default widget: ``FileInput``
+    * Empty value: ``None``
+    * Normalizes to: An ``UploadedFile`` object that wraps the file content
+      and file name into a single object.
+    * Validates that file data has been bound to the form, and that the
+      file is of an image format understood by PIL.
+
+Using an ImageField requires that the `Python Imaging Library`_ is installed.
+
+When you use a ``FileField`` on a form, you must also remember to
+`bind the file data to the form`_.
+
+.. _Python Imaging Library: http://www.pythonware.com/products/pil/
+
 ``IntegerField``
 ~~~~~~~~~~~~~~~~
 
@@ -1378,11 +1463,11 @@ the full list of conversions:
     ``DateTimeField``                ``DateTimeField``
     ``DecimalField``                 ``DecimalField``
     ``EmailField``                   ``EmailField``
-    ``FileField``                    ``CharField``
+    ``FileField``                    ``FileField``
     ``FilePathField``                ``CharField``
     ``FloatField``                   ``FloatField``
     ``ForeignKey``                   ``ModelChoiceField`` (see below)
-    ``ImageField``                   ``CharField``
+    ``ImageField``                   ``ImageField``
     ``IntegerField``                 ``IntegerField``
     ``IPAddressField``               ``CharField``
     ``ManyToManyField``              ``ModelMultipleChoiceField`` (see
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 52cc9ccfa5..2386a7f8b1 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -173,27 +173,29 @@ u'<input type="hidden" class="special" value="foo@example.com" name="email" />'
 
 # FileInput Widget ############################################################
 
+FileInput widgets don't ever show the value, because the old value is of no use
+if you are updating the form or if the provided file generated an error.
 >>> w = FileInput()
 >>> w.render('email', '')
 u'<input type="file" name="email" />'
 >>> w.render('email', None)
 u'<input type="file" name="email" />'
 >>> w.render('email', 'test@example.com')
-u'<input type="file" name="email" value="test@example.com" />'
+u'<input type="file" name="email" />'
 >>> w.render('email', 'some "quoted" & ampersanded value')
-u'<input type="file" name="email" value="some &quot;quoted&quot; &amp; ampersanded value" />'
+u'<input type="file" name="email" />'
 >>> w.render('email', 'test@example.com', attrs={'class': 'fun'})
-u'<input type="file" name="email" value="test@example.com" class="fun" />'
+u'<input type="file" name="email" class="fun" />'
 
 You can also pass 'attrs' to the constructor:
 >>> w = FileInput(attrs={'class': 'fun'})
 >>> w.render('email', '')
 u'<input type="file" class="fun" name="email" />'
 >>> w.render('email', 'foo@example.com')
-u'<input type="file" class="fun" value="foo@example.com" name="email" />'
+u'<input type="file" class="fun" name="email" />'
 
 >>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'})
-u'<input type="file" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />'
+u'<input type="file" class="fun" name="email" />'
 
 # Textarea Widget #############################################################
 
@@ -1532,6 +1534,42 @@ Traceback (most recent call last):
 ...
 ValidationError: [u'Ensure this value has at most 15 characters (it has 20).']
 
+# FileField ##################################################################
+
+>>> f = FileField()
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f.clean({})
+Traceback (most recent call last):
+...
+ValidationError: [u'No file was submitted.']
+
+>>> f.clean('some content that is not a file')
+Traceback (most recent call last):
+...
+ValidationError: [u'No file was submitted. Check the encoding type on the form.']
+
+>>> f.clean({'filename': 'name', 'content':None})
+Traceback (most recent call last):
+...
+ValidationError: [u'The submitted file is empty.']
+
+>>> f.clean({'filename': 'name', 'content':''})
+Traceback (most recent call last):
+...
+ValidationError: [u'The submitted file is empty.']
+
+>>> type(f.clean({'filename': 'name', 'content':'Some File Content'}))
+<class 'django.newforms.fields.UploadedFile'>
+
 # URLField ##################################################################
 
 >>> f = URLField()
@@ -2573,7 +2611,7 @@ Instances of a dynamic Form do not persist fields from one Form instance to
 the next.
 >>> class MyForm(Form):
 ...     def __init__(self, data=None, auto_id=False, field_list=[]):
-...         Form.__init__(self, data, auto_id)
+...         Form.__init__(self, data, auto_id=auto_id)
 ...         for field in field_list:
 ...             self.fields[field[0]] = field[1]
 >>> field_list = [('field1', CharField()), ('field2', CharField())]
@@ -2591,7 +2629,7 @@ the next.
 ...     default_field_1 = CharField()
 ...     default_field_2 = CharField()
 ...     def __init__(self, data=None, auto_id=False, field_list=[]):
-...         Form.__init__(self, data, auto_id)
+...         Form.__init__(self, data, auto_id=auto_id)
 ...         for field in field_list:
 ...             self.fields[field[0]] = field[1]
 >>> field_list = [('field1', CharField()), ('field2', CharField())]
@@ -3246,6 +3284,35 @@ is different than its data. This is handled transparently, though.
 <option value="3" selected="selected">No</option>
 </select>
 
+# Forms with FileFields ################################################
+
+FileFields are a special case because they take their data from the request.FILES,
+not request.POST. 
+
+>>> class FileForm(Form):
+...     file1 = FileField()
+>>> f = FileForm(auto_id=False)
+>>> print f
+<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>
+
+>>> f = FileForm(data={}, files={}, auto_id=False)
+>>> print f
+<tr><th>File1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="file" name="file1" /></td></tr>
+
+>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':''}}, auto_id=False)
+>>> print f
+<tr><th>File1:</th><td><ul class="errorlist"><li>The submitted file is empty.</li></ul><input type="file" name="file1" /></td></tr>
+
+>>> f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False)
+>>> print f
+<tr><th>File1:</th><td><ul class="errorlist"><li>No file was submitted. Check the encoding type on the form.</li></ul><input type="file" name="file1" /></td></tr>
+
+>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False)
+>>> print f
+<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>
+>>> f.is_valid()
+True
+
 # Basic form processing in a view #############################################
 
 >>> from django.template import Template, Context