diff --git a/django/contrib/comments/models/comments.py b/django/contrib/comments/models/comments.py
index 83b60f88c3..b9ee199156 100644
--- a/django/contrib/comments/models/comments.py
+++ b/django/contrib/comments/models/comments.py
@@ -78,7 +78,7 @@ class Comment(meta.Model):
         """
         from django.core.exceptions import ObjectDoesNotExist
         try:
-            return self.get_content_type().get_object_for_this_type(id__exact=self.object_id)
+            return self.get_content_type().get_object_for_this_type(pk=self.object_id)
         except ObjectDoesNotExist:
             return None
 
@@ -193,7 +193,7 @@ class FreeComment(meta.Model):
         """
         from django.core.exceptions import ObjectDoesNotExist
         try:
-            return self.get_content_type().get_object_for_this_type(id__exact=self.object_id)
+            return self.get_content_type().get_object_for_this_type(pk=self.object_id)
         except ObjectDoesNotExist:
             return None
 
diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py
index 99bf50fd12..bb7150fc17 100644
--- a/django/contrib/comments/templatetags/comments.py
+++ b/django/contrib/comments/templatetags/comments.py
@@ -80,7 +80,7 @@ class CommentFormNode(template.Node):
             # We only have to do this validation if obj_id_lookup_var is provided,
             # because do_comment_form() validates hard-coded object IDs.
             try:
-                self.content_type.get_object_for_this_type(id__exact=self.obj_id)
+                self.content_type.get_object_for_this_type(pk=self.obj_id)
             except ObjectDoesNotExist:
                 context['display_form'] = False
             else:
@@ -203,7 +203,7 @@ class DoCommentForm:
         if tokens[3].isdigit():
             obj_id = tokens[3]
             try: # ensure the object ID is valid
-                content_type.get_object_for_this_type(id__exact=obj_id)
+                content_type.get_object_for_this_type(pk=obj_id)
             except ObjectDoesNotExist:
                 raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
         else:
@@ -283,7 +283,7 @@ class DoCommentCount:
         if tokens[3].isdigit():
             obj_id = tokens[3]
             try: # ensure the object ID is valid
-                content_type.get_object_for_this_type(id__exact=obj_id)
+                content_type.get_object_for_this_type(pk=obj_id)
             except ObjectDoesNotExist:
                 raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
         else:
@@ -338,7 +338,7 @@ class DoGetCommentList:
         if tokens[3].isdigit():
             obj_id = tokens[3]
             try: # ensure the object ID is valid
-                content_type.get_object_for_this_type(id__exact=obj_id)
+                content_type.get_object_for_this_type(pk=obj_id)
             except ObjectDoesNotExist:
                 raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
         else:
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index 1a28676a88..c14df2cfdd 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -197,7 +197,7 @@ def post_comment(request):
         rating_range, rating_choices = [], []
     content_type_id, object_id = target.split(':') # target is something like '52:5157'
     try:
-        obj = contenttypes.get_object(id__exact=content_type_id).get_object_for_this_type(id__exact=object_id)
+        obj = contenttypes.get_object(pk=content_type_id).get_object_for_this_type(pk=object_id)
     except ObjectDoesNotExist:
         raise Http404, "The comment form had an invalid 'target' parameter -- the object ID was invalid"
     option_list = options.split(',') # options is something like 'pa,ra'
@@ -284,9 +284,9 @@ def post_free_comment(request):
     if comments.get_security_hash(options, '', '', target) != security_hash:
         raise Http404, "Somebody tampered with the comment form (security violation)"
     content_type_id, object_id = target.split(':') # target is something like '52:5157'
-    content_type = contenttypes.get_object(id__exact=content_type_id)
+    content_type = contenttypes.get_object(pk=content_type_id)
     try:
-        obj = content_type.get_object_for_this_type(id__exact=object_id)
+        obj = content_type.get_object_for_this_type(pk=object_id)
     except ObjectDoesNotExist:
         raise Http404, "The comment form had an invalid 'target' parameter -- the object ID was invalid"
     option_list = options.split(',')
@@ -336,8 +336,8 @@ def comment_was_posted(request):
     if request.GET.has_key('c'):
         content_type_id, object_id = request.GET['c'].split(':')
         try:
-            content_type = contenttypes.get_object(id__exact=content_type_id)
-            obj = content_type.get_object_for_this_type(id__exact=object_id)
+            content_type = contenttypes.get_object(pk=content_type_id)
+            obj = content_type.get_object_for_this_type(pk=object_id)
         except ObjectDoesNotExist:
             pass
     t = template_loader.get_template('comments/posted')
diff --git a/django/contrib/comments/views/karma.py b/django/contrib/comments/views/karma.py
index afc115c0ef..7422047fad 100644
--- a/django/contrib/comments/views/karma.py
+++ b/django/contrib/comments/views/karma.py
@@ -19,14 +19,14 @@ def vote(request, comment_id, vote):
     if request.user.is_anonymous():
         raise Http404, "Anonymous users cannot vote"
     try:
-        comment = comments.get_object(id__exact=comment_id)
+        comment = comments.get_object(pk=comment_id)
     except comments.CommentDoesNotExist:
         raise Http404, "Invalid comment ID"
     if comment.user_id == request.user.id:
         raise Http404, "No voting for yourself"
     karma.vote(request.user.id, comment_id, rating)
     # Reload comment to ensure we have up to date karma count
-    comment = comments.get_object(id__exact=comment_id)
+    comment = comments.get_object(pk=comment_id)
     t = template_loader.get_template('comments/karma_vote_accepted')
     c = Context(request, {
         'comment': comment
diff --git a/django/contrib/comments/views/userflags.py b/django/contrib/comments/views/userflags.py
index f0d0715cdd..59664f3cab 100644
--- a/django/contrib/comments/views/userflags.py
+++ b/django/contrib/comments/views/userflags.py
@@ -16,7 +16,7 @@ def flag(request, comment_id):
             the flagged `comments.comments` object
     """
     try:
-        comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+        comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID)
     except comments.CommentDoesNotExist:
         raise Http404
     if request.POST:
@@ -31,7 +31,7 @@ flag = login_required(flag)
 
 def flag_done(request, comment_id):
     try:
-        comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+        comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID)
     except comments.CommentDoesNotExist:
         raise Http404
     t = template_loader.get_template('comments/flag_done')
@@ -50,7 +50,7 @@ def delete(request, comment_id):
             the flagged `comments.comments` object
     """
     try:
-        comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+        comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID)
     except comments.CommentDoesNotExist:
         raise Http404
     if not comments.user_is_moderator(request.user):
@@ -72,7 +72,7 @@ delete = login_required(delete)
 
 def delete_done(request, comment_id):
     try:
-        comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+        comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID)
     except comments.CommentDoesNotExist:
         raise Http404
     t = template_loader.get_template('comments/delete_done')
diff --git a/django/core/meta.py b/django/core/meta.py
index f16a5ad20b..51cdee2b63 100644
--- a/django/core/meta.py
+++ b/django/core/meta.py
@@ -1146,6 +1146,9 @@ def _parse_lookup(kwarg_items, opts, table_count=0):
                 params.extend(params2)
             continue
         lookup_list = kwarg.split(LOOKUP_SEPARATOR)
+        # pk="value" is shorthand for (primary key)__exact="value"
+        if lookup_list[-1] == 'pk':
+            lookup_list = lookup_list[:-1] + [opts.pk.name, 'exact']
         if len(lookup_list) == 1:
             _throw_bad_kwarg_error(kwarg)
         lookup_type = lookup_list.pop()
diff --git a/django/models/auth.py b/django/models/auth.py
index d0d2bf5002..63e879fdea 100644
--- a/django/models/auth.py
+++ b/django/models/auth.py
@@ -222,7 +222,7 @@ class Session(meta.Model):
         "Sets the necessary cookie in the given HttpResponse object, also updates last login time for user."
         from django.models.auth import users
         from django.conf.settings import REGISTRATION_COOKIE_DOMAIN
-        user = users.get_object(id__exact=user_id)
+        user = users.get_object(pk=user_id)
         user.last_login = datetime.datetime.now()
         user.save()
         session = create_session(user_id)
@@ -274,7 +274,7 @@ class LogEntry(meta.Model):
 
     def get_edited_object(self):
         "Returns the edited object represented by this log entry"
-        return self.get_content_type().get_object_for_this_type(id__exact=self.object_id)
+        return self.get_content_type().get_object_for_this_type(pk=self.object_id)
 
     def get_admin_url(self):
         """
diff --git a/django/models/core.py b/django/models/core.py
index bf74610db2..18c9e90072 100644
--- a/django/models/core.py
+++ b/django/models/core.py
@@ -14,7 +14,7 @@ class Site(meta.Model):
     def _module_get_current():
         "Returns the current site, according to the SITE_ID constant."
         from django.conf.settings import SITE_ID
-        return get_object(id__exact=SITE_ID)
+        return get_object(pk=SITE_ID)
 
 class Package(meta.Model):
     db_table = 'packages'
diff --git a/django/views/admin/doc.py b/django/views/admin/doc.py
index 3b593e5944..8395ebc740 100644
--- a/django/views/admin/doc.py
+++ b/django/views/admin/doc.py
@@ -1,6 +1,3 @@
-import os
-import re
-import inspect
 from django.core import meta
 from django import templatetags
 from django.conf import settings
@@ -14,6 +11,7 @@ try:
     from django.parts.admin import doc
 except ImportError:
     doc = None
+import inspect, os, re
 
 # Exclude methods starting with these strings from documentation
 MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_')
@@ -128,7 +126,7 @@ def view_index(request):
                 'module' : func.__module__,
                 'title'  : title,
                 'site_id': settings_mod.SITE_ID,
-                'site'   : sites.get_object(id__exact=settings_mod.SITE_ID),
+                'site'   : sites.get_object(pk=settings_mod.SITE_ID),
                 'url'    : simplify_regex(regex),
             })
     t = template_loader.get_template('doc/view_index')
diff --git a/django/views/defaults.py b/django/views/defaults.py
index 7d362f18b2..4da2643c3d 100644
--- a/django/views/defaults.py
+++ b/django/views/defaults.py
@@ -7,8 +7,8 @@ from django.utils import httpwrappers
 def shortcut(request, content_type_id, object_id):
     from django.models.core import contenttypes
     try:
-        content_type = contenttypes.get_object(id__exact=content_type_id)
-        obj = content_type.get_object_for_this_type(id__exact=object_id)
+        content_type = contenttypes.get_object(pk=content_type_id)
+        obj = content_type.get_object_for_this_type(pk=object_id)
     except ObjectDoesNotExist:
         raise Http404, "Content type %s object %s doesn't exist" % (content_type_id, object_id)
     if not hasattr(obj, 'get_absolute_url'):
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 401d9d3260..cb5ec4783f 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -97,6 +97,19 @@ Multiple lookups are allowed, of course, and are translated as "AND"s::
 
 ...retrieves all polls published in January 2005 that have a question starting with "Would."
 
+For convenience, there's a ``pk`` lookup type, which translates into
+``(primary_key)__exact``. In the polls example, these two statements are
+equivalent::
+
+    polls.get_object(id__exact=3)
+    polls.get_object(pk=3)
+
+``pk`` lookups also work across joins. In the polls example, these two
+statements are equivalent::
+
+    choices.get_list(poll__id__exact=3)
+    choices.get_list(poll__pk=3)
+
 Ordering
 ========
 
diff --git a/docs/overview.txt b/docs/overview.txt
index 86f089778d..7fba5e1767 100644
--- a/docs/overview.txt
+++ b/docs/overview.txt
@@ -93,6 +93,12 @@ is created on the fly: No code generation necessary::
         ...
     django.models.news.ReporterDoesNotExist: Reporter does not exist for {'id__exact': 2}
 
+    # Lookup by a primary key is the most common case, so Django provides a
+    # shortcut for primary-key exact lookups.
+    # The following is identical to reporters.get_object(id__exact=1).
+    >>> reporters.get_object(pk=1)
+    John Smith
+
     # Create an article.
     >>> from datetime import datetime
     >>> a = articles.Article(id=None, pub_date=datetime.now(), headline='Django is cool', article='Yeah.', reporter_id=1)
@@ -200,7 +206,7 @@ article_detail from above::
     def article_detail(request, year, month, article_id):
         # Use the Django API to find an object matching the URL criteria.
         try:
-            a = articles.get_object(pub_date__year=year, pub_date__month=month, id__exact=article_id)
+            a = articles.get_object(pub_date__year=year, pub_date__month=month, pk=article_id)
         except articles.ArticleDoesNotExist:
             raise Http404
         t = template_loader.get_template('news/article_detail')
diff --git a/docs/tutorial01.txt b/docs/tutorial01.txt
index 4e377bd97d..e92e9c2ccd 100644
--- a/docs/tutorial01.txt
+++ b/docs/tutorial01.txt
@@ -49,10 +49,10 @@ settings. Let's look at what ``startproject`` created::
 First, edit ``myproject/settings/main.py``. It's a normal Python module with
 module-level variables representing Django settings. Edit the file and change
 these settings to match your database's connection parameters:
-    
-    * ``DATABASE_ENGINE`` -- Either 'postgresql', 'mysql' or 'sqlite3'. 
+
+    * ``DATABASE_ENGINE`` -- Either 'postgresql', 'mysql' or 'sqlite3'.
       More coming soon.
-    * ``DATABASE_NAME`` -- The name of your database, or the full path to 
+    * ``DATABASE_NAME`` -- The name of your database, or the full path to
       the database file if using sqlite.
     * ``DATABASE_USER`` -- Your database username (not used for sqlite).
     * ``DATABASE_PASSWORD`` -- Your database password (not used for sqlite).
@@ -134,7 +134,7 @@ The first step in writing a database Web app in Django is to define your models
 -- essentially, your database layout, with additional metadata.
 
 .. admonition:: Philosophy
-   
+
    A model is the single, definitive source of data about your
    data. It contains the essential fields and behaviors of the data you're
    storing. Django follows the `DRY Principle`_. The goal is to define your
@@ -243,11 +243,11 @@ Note the following:
     * Table names are automatically generated by combining the name of the app
       (polls) with a plural version of the object name (polls and choices). (You
       can override this behavior.)
-      
+
     * Primary keys (IDs) are added automatically. (You can override this, too.)
-    
+
     * The foreign key relationship is made explicit by a ``REFERENCES`` statement.
-    
+
     * It's tailored to the database you're using, so database-specific field types
       such as ``auto_increment`` (MySQL), ``serial`` (PostgreSQL), or ``integer
       primary key`` (SQLite) are handled for you automatically. The author of
@@ -256,16 +256,16 @@ Note the following:
 
 If you're interested, also run the following commands:
 
-    * ``django-admin.py sqlinitialdata polls`` -- Outputs the initial-data 
+    * ``django-admin.py sqlinitialdata polls`` -- Outputs the initial-data
       inserts required for Django's admin framework.
-    
-    * ``django-admin.py sqlclear polls`` -- Outputs the necessary ``DROP 
+
+    * ``django-admin.py sqlclear polls`` -- Outputs the necessary ``DROP
       TABLE`` statements for this app, according to which tables already exist
       in your database (if any).
-      
+
     * ``django-admin.py sqlindexes polls`` -- Outputs the ``CREATE INDEX``
       statements for this app.
-    
+
     * ``django-admin.py sqlall polls`` -- A combination of 'sql' and
       'sqlinitialdata'.
 
@@ -372,14 +372,20 @@ Let's jump back into the Python interactive shell::
     >>> polls.get_list(question__startswith='What')
     [What's up]
 
+    # Lookup by a primary key is the most common case, so Django provides a
+    # shortcut for primary-key exact lookups.
+    # The following is identical to polls.get_object(id__exact=1).
+    >>> polls.get_object(pk=1)
+    What's up
+
     # Make sure our custom method worked.
-    >>> p = polls.get_object(id__exact=1)
+    >>> p = polls.get_object(pk=1)
     >>> p.was_published_today()
     False
 
     # Give the Poll a couple of Choices. Each one of these method calls does an
     # INSERT statement behind the scenes and returns the new Choice object.
-    >>> p = polls.get_object(id__exact=1)
+    >>> p = polls.get_object(pk=1)
     >>> p.add_choice(choice='Not much', votes=0)
     Not much
     >>> p.add_choice(choice='The sky', votes=0)
diff --git a/docs/tutorial03.txt b/docs/tutorial03.txt
index 1c547a670f..3f2f97b7b1 100644
--- a/docs/tutorial03.txt
+++ b/docs/tutorial03.txt
@@ -242,7 +242,7 @@ for a given poll. Here's the view::
     from django.core.exceptions import Http404
     def detail(request, poll_id):
         try:
-            p = polls.get_object(id__exact=poll_id)
+            p = polls.get_object(pk=poll_id)
         except polls.PollDoesNotExist:
             raise Http404
         t = template_loader.get_template('polls/detail')