diff --git a/django/core/apps/base.py b/django/core/apps/base.py
index 2503d9e3ff..706ed54ef6 100644
--- a/django/core/apps/base.py
+++ b/django/core/apps/base.py
@@ -1,6 +1,7 @@
 from importlib import import_module
 
-from django.utils.module_loading import module_has_submodule
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.module_loading import import_by_path, module_has_submodule
 from django.utils._os import upath
 
 
@@ -14,16 +15,33 @@ class AppConfig(object):
 
     def __init__(self, app_name):
         # Full Python path to the application eg. 'django.contrib.admin'.
-        # This is the value that appears in INSTALLED_APPS.
         self.name = app_name
 
+        # Root module for the application eg. <module 'django.contrib.admin'
+        # from 'django/contrib/admin/__init__.pyc'>.
+        self.app_module = import_module(app_name)
+
+        # The following attributes could be defined at the class level in a
+        # subclass, hence the test-and-set pattern.
+
         # Last component of the Python path to the application eg. 'admin'.
         # This value must be unique across a Django project.
-        self.label = app_name.rpartition(".")[2]
+        if not hasattr(self, 'label'):
+            self.label = app_name.rpartition(".")[2]
 
-        # Root module eg. <module 'django.contrib.admin' from
-        # 'django/contrib/admin/__init__.pyc'>.
-        self.app_module = import_module(app_name)
+        # Human-readable name for the application eg. "Admin".
+        if not hasattr(self, 'verbose_name'):
+            self.verbose_name = self.label.title()
+
+        # Filesystem path to the application directory eg.
+        # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. May be
+        # None if the application isn't a bona fide package eg. if it's an
+        # egg. Otherwise it's a unicode on Python 2 and a str on Python 3.
+        if not hasattr(self, 'path'):
+            try:
+                self.path = upath(self.app_module.__path__[0])
+            except AttributeError:
+                self.path = None
 
         # Module containing models eg. <module 'django.contrib.admin.models'
         # from 'django/contrib/admin/models.pyc'>. Set by import_models().
@@ -34,18 +52,49 @@ class AppConfig(object):
         # None to prevent accidental access before import_models() runs.
         self.models = None
 
-        # Filesystem path to the application directory eg.
-        # u'/usr/lib/python2.7/dist-packages/django/contrib/admin'. May be
-        # None if the application isn't a bona fide package eg. if it's an
-        # egg. Otherwise it's a unicode on Python 2 and a str on Python 3.
-        try:
-            self.path = upath(self.app_module.__path__[0])
-        except AttributeError:
-            self.path = None
-
     def __repr__(self):
         return '<AppConfig: %s>' % self.label
 
+    @classmethod
+    def create(cls, entry):
+        """
+        Factory that creates an app config from an entry in INSTALLED_APPS.
+        """
+        try:
+            # If import_module succeeds, entry is a path to an app module.
+            # Otherwise, entry is a path to an app config class or an error.
+            import_module(entry)
+
+        except ImportError:
+            # Raise the original exception when entry cannot be a path to an
+            # app config class. Since module names are allowable here, the
+            # standard exception message from import_by_path is unsuitable.
+            if '.' not in entry:
+                raise
+
+            cls = import_by_path(entry)
+
+            # Check for obvious errors. (This check prevents duck typing, but
+            # it could be removed if it became a problem in practice.)
+            if not issubclass(cls, AppConfig):
+                raise ImproperlyConfigured(
+                    "%r isn't a subclass of AppConfig." % entry)
+
+            # Obtain app name here rather than in AppClass.__init__ to keep
+            # all error checking for entries in INSTALLED_APPS in one place.
+            try:
+                app_name = cls.name
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    "%r must supply a name attribute." % entry)
+
+            # Entry is a path to an app config class.
+            return cls(app_name)
+
+        else:
+            # Entry is a path to an app module.
+            return cls(entry)
+
     def import_models(self, all_models):
         # Dictionary of models for this app, stored in the 'all_models'
         # attribute of the AppCache this AppConfig is attached to. Injected as
diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py
index 92b9f8d8d4..056aa9a60b 100644
--- a/django/core/apps/cache.py
+++ b/django/core/apps/cache.py
@@ -83,7 +83,7 @@ class AppCache(object):
             # especially not other application modules, even indirectly.
             # Therefore we simply import them sequentially.
             for app_name in settings.INSTALLED_APPS:
-                app_config = AppConfig(app_name)
+                app_config = AppConfig.create(app_name)
                 self.app_configs[app_config.label] = app_config
 
             self._apps_loaded = True
@@ -347,7 +347,7 @@ class AppCache(object):
 
     def _begin_with_app(self, app_name):
         # Returns an opaque value that can be passed to _end_with_app().
-        app_config = AppConfig(app_name)
+        app_config = AppConfig.create(app_name)
         if app_config.label in self.app_configs:
             return None
         else:
@@ -412,7 +412,7 @@ class AppCache(object):
         warnings.warn(
             "load_app(app_name) is deprecated.",
             PendingDeprecationWarning, stacklevel=2)
-        app_config = AppConfig(app_name)
+        app_config = AppConfig.create(app_name)
         app_config.import_models(self.all_models[app_config.label])
         self.app_configs[app_config.label] = app_config
         return app_config.models_module
diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py
index a6dbeb0ae4..3c365b0225 100644
--- a/tests/test_runner/tests.py
+++ b/tests/test_runner/tests.py
@@ -226,7 +226,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase)
         "Check that the get_tests helper function can find tests in a directory"
         from django.core.apps.base import AppConfig
         from django.test.simple import get_tests
-        app_config = AppConfig('test_runner.valid_app')
+        app_config = AppConfig.create('test_runner.valid_app')
         app_config.import_models({})
         tests = get_tests(app_config)
         self.assertIsInstance(tests, types.ModuleType)
@@ -235,7 +235,7 @@ class ModulesTestsPackages(IgnoreAllDeprecationWarningsMixin, unittest.TestCase)
         "Test for #12658 - Tests with ImportError's shouldn't fail silently"
         from django.core.apps.base import AppConfig
         from django.test.simple import get_tests
-        app_config = AppConfig('test_runner_invalid_app')
+        app_config = AppConfig.create('test_runner_invalid_app')
         app_config.import_models({})
         with self.assertRaises(ImportError):
             get_tests(app_config)