diff --git a/bottle.py b/bottle.py
index e4b17d8..5287147 100644
--- a/bottle.py
+++ b/bottle.py
@@ -2176,6 +2176,22 @@ class ConfigDict(dict):
         self._meta = {}
         self._on_change = lambda name, value: None
 
+    def load_module(self, path, squash):
+        """ Load values from a Python module.
+            :param squash: Squash nested dicts into namespaces by using
+                           load_dict(), otherwise use update()
+            Example: load_config('my.app.settings', True)
+            Example: load_config('my.app.settings', False)
+        """
+        config_obj = __import__(path)
+        obj = dict([(key, getattr(config_obj, key))
+                for key in dir(config_obj) if key.isupper()])
+        if squash:
+            self.load_dict(obj)
+        else:
+            self.update(obj)
+        return self
+
     def load_config(self, filename):
         """ Load values from an ``*.ini`` style config file.
 

Ability to load config from module
Currently it is not possible to load config from a Python module (similar to the way Django does it).

I'd like to propose the following;

```py
class ConfigDict(dict):
    def load_module(self, module_path):
        """
        Load configuration from module path

        >>> load_module('settings.prod')
        """
        assert isinstance(module_path, str)
        config = importlib.import_module(module_path)
        obj = {key: getattr(config, key) for key in dir(config) if key.isupper()}
        self.update(obj)
```

Happy to put together a tested PR if this proposal is accepted
diff --git a/test/example_settings.py b/test/example_settings.py
new file mode 100644
index 0000000..58dd992
--- /dev/null
+++ b/test/example_settings.py
@@ -0,0 +1,5 @@
+A = {
+    "B": {
+        "C": 3
+    }
+}
\ No newline at end of file
diff --git a/test/test_config.py b/test/test_config.py
index a4cfd9f..a9ea4ab 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -1,3 +1,4 @@
+import sys
 import unittest
 from bottle import ConfigDict
 
@@ -69,6 +70,17 @@ class TestConfDict(unittest.TestCase):
         c = ConfigDict()
         c.load_dict({key: {'subkey': 'value'}})
         self.assertEqual('value', c[key + '.subkey'])
+
+    def test_load_module(self):
+        c = ConfigDict()
+        c.load_module('example_settings', True)
+        self.assertEqual(c['A.B.C'], 3)
+
+        c = ConfigDict()
+        c.load_module('example_settings', False)
+        self.assertEqual(c['A']['B']['C'], 3)
+
+
    
 if __name__ == '__main__': #pragma: no cover
     unittest.main()

pip install -e .[dev]
parse_log_pytest
pytest
Mako jinja2 eventlet cherrypy paste twisted tornado pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
autocommand==2.2.2
Automat==24.8.1
backports.tarfile==1.2.0
-e git+https://github.com/bottlepy/bottle.git@534a2e08ac0ef55dd542a3a83b1118188c6a399b#egg=bottle
cheroot==10.0.1
CherryPy==18.10.0
constantly==23.10.4
dnspython==2.7.0
eventlet==0.39.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
greenlet==3.1.1
hyperlink==21.0.0
idna==3.10
incremental==24.7.2
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jaraco.collections==5.1.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jaraco.text==4.0.0
Jinja2==3.1.6
Mako==1.3.9
MarkupSafe==3.0.2
more-itertools==10.6.0
packaging @ file:///croot/packaging_1734472117206/work
Paste==3.10.1
pluggy @ file:///croot/pluggy_1733169602837/work
portend==3.2.0
pytest @ file:///croot/pytest_1738938843180/work
python-dateutil==2.9.0.post0
six==1.17.0
tempora==5.8.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tornado==6.4.2
Twisted==24.11.0
typing_extensions==4.13.0
zc.lockfile==3.0.post1
zope.interface==7.2

name: bottle
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - autocommand==2.2.2
      - automat==24.8.1
      - backports-tarfile==1.2.0
      - cheroot==10.0.1
      - cherrypy==18.10.0
      - constantly==23.10.4
      - dnspython==2.7.0
      - eventlet==0.39.1
      - greenlet==3.1.1
      - hyperlink==21.0.0
      - idna==3.10
      - incremental==24.7.2
      - jaraco-collections==5.1.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jaraco-text==4.0.0
      - jinja2==3.1.6
      - mako==1.3.9
      - markupsafe==3.0.2
      - more-itertools==10.6.0
      - paste==3.10.1
      - portend==3.2.0
      - python-dateutil==2.9.0.post0
      - six==1.17.0
      - tempora==5.8.0
      - tornado==6.4.2
      - twisted==24.11.0
      - typing-extensions==4.13.0
      - zc-lockfile==3.0.post1
      - zope-interface==7.2
prefix: /opt/conda/envs/bottle

test/test_config.py::TestConfDict::test_load_module
test/test_config.py::TestConfDict::test_isadict test/test_config.py::TestConfDict::test_load_dict test/test_config.py::TestConfDict::test_meta test/test_config.py::TestConfDict::test_namespaces test/test_config.py::TestConfDict::test_update test/test_config.py::TestConfDict::test_write
bottle.py
bottle.py