Add ignored modules to Astroid module deny list (#9504)

diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt
index fd4fed0..4bcff9c 100644
--- a/.pyenchant_pylint_custom_dict.txt
+++ b/.pyenchant_pylint_custom_dict.txt
@@ -300,6 +300,7 @@
 sqlalchemy
 src
 starargs
+stateful
 staticmethod
 stderr
 stdin
diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst
index 94d2c17..cef8e2a 100644
--- a/doc/user_guide/configuration/all-options.rst
+++ b/doc/user_guide/configuration/all-options.rst
@@ -120,7 +120,7 @@
 
 --ignored-modules
 """""""""""""""""
-*List of module names for which member attributes should not be checked (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.*
+*List of module names for which member attributes should not be checked and will not be imported (useful for modules/projects where namespaces are manipulated during runtime and thus existing member attributes cannot be deduced by static analysis). It supports qualified module names, as well as Unix pattern matching.*
 
 **Default:**  ``()``
 
diff --git a/doc/whatsnew/fragments/9442.other b/doc/whatsnew/fragments/9442.other
new file mode 100644
index 0000000..9523aba
--- /dev/null
+++ b/doc/whatsnew/fragments/9442.other
@@ -0,0 +1,5 @@
+Ignored modules are now not checked at all, instead of being checked and then
+ignored. This should speed up the analysis of large codebases which have
+ignored modules.
+
+Closes #9442 (`#9442 <https://github.com/pylint-dev/pylint/issues/9442>`_)
diff --git a/examples/pylintrc b/examples/pylintrc
index d478005..4685415 100644
--- a/examples/pylintrc
+++ b/examples/pylintrc
@@ -59,10 +59,11 @@
 # Emacs file locks
 ignore-patterns=^\.#
 
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis). It
-# supports qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 ignored-modules=
 
 # Python code to execute, usually for sys.path manipulation such as
diff --git a/examples/pyproject.toml b/examples/pyproject.toml
index a8ec9a7..7d9ffe8 100644
--- a/examples/pyproject.toml
+++ b/examples/pyproject.toml
@@ -49,10 +49,11 @@
 # file locks
 ignore-patterns = ["^\\.#"]
 
-# List of module names for which member attributes should not be checked (useful
-# for modules/projects where namespaces are manipulated during runtime and thus
-# existing member attributes cannot be deduced by static analysis). It supports
-# qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 # ignored-modules =
 
 # Python code to execute, usually for sys.path manipulation such as
diff --git a/pylint/lint/base_options.py b/pylint/lint/base_options.py
index 3d5ba5d..cd354c4 100644
--- a/pylint/lint/base_options.py
+++ b/pylint/lint/base_options.py
@@ -384,7 +384,8 @@
                 "type": "csv",
                 "metavar": "<module names>",
                 "help": "List of module names for which member attributes "
-                "should not be checked (useful for modules/projects "
+                "should not be checked and will not be imported "
+                "(useful for modules/projects "
                 "where namespaces are manipulated during runtime and "
                 "thus existing member attributes cannot be "
                 "deduced by static analysis). It supports qualified "
diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py
index 3025015..f1aca76 100644
--- a/pylint/lint/pylinter.py
+++ b/pylint/lint/pylinter.py
@@ -1073,6 +1073,7 @@
         MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
         MANAGER.max_inferable_values = self.config.limit_inference_results
         MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list)
+        MANAGER.module_denylist.update(self.config.ignored_modules)
         if self.config.extension_pkg_whitelist:
             MANAGER.extension_package_whitelist.update(
                 self.config.extension_pkg_whitelist
diff --git a/pylintrc b/pylintrc
index 434fa23..a943b1c 100644
--- a/pylintrc
+++ b/pylintrc
@@ -342,10 +342,11 @@
 # members is set to 'yes'
 mixin-class-rgx=.*MixIn
 
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis). It
-# supports qualified module names, as well as Unix pattern matching.
+# List of module names for which member attributes should not be checked and
+# will not be imported (useful for modules/projects where namespaces are
+# manipulated during runtime and thus existing member attributes cannot be
+# deduced by static analysis). It supports qualified module names, as well
+# as Unix pattern matching.
 ignored-modules=
 
 # List of class names for which member attributes should not be checked (useful
diff --git a/tests/lint/test_pylinter.py b/tests/lint/test_pylinter.py
index 1e4f40a..c14d192 100644
--- a/tests/lint/test_pylinter.py
+++ b/tests/lint/test_pylinter.py
@@ -10,7 +10,7 @@
 
 from pytest import CaptureFixture
 
-from pylint.lint.pylinter import PyLinter
+from pylint.lint.pylinter import MANAGER, PyLinter
 from pylint.utils import FileState
 
 
@@ -48,3 +48,14 @@
         assert len(files) == 1
         assert "pylint-crash-20" in str(files[0])
         assert any(m.symbol == "astroid-error" for m in linter.reporter.messages)
+
+
+def test_open_pylinter_denied_modules(linter: PyLinter) -> None:
+    """Test PyLinter open() adds ignored modules to Astroid manager deny list."""
+    MANAGER.module_denylist = {"mod1"}
+    try:
+        linter.config.ignored_modules = ["mod2", "mod3"]
+        linter.open()
+        assert MANAGER.module_denylist == {"mod1", "mod2", "mod3"}
+    finally:
+        MANAGER.module_denylist = set()
diff --git a/tests/test_functional.py b/tests/test_functional.py
index ef0a373..df42767 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -7,13 +7,16 @@
 from __future__ import annotations
 
 import sys
+from collections.abc import Iterator
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 import pytest
 from _pytest.config import Config
 
 from pylint import testutils
 from pylint.constants import PY312_PLUS
+from pylint.lint.pylinter import MANAGER
 from pylint.testutils import UPDATE_FILE, UPDATE_OPTION
 from pylint.testutils.functional import (
     FunctionalTestFile,
@@ -22,6 +25,9 @@
 )
 from pylint.utils import HAS_ISORT_5
 
+if TYPE_CHECKING:
+    from pylint.lint import PyLinter
+
 FUNCTIONAL_DIR = Path(__file__).parent.resolve() / "functional"
 
 
@@ -40,6 +46,14 @@
 ]
 
 
+@pytest.fixture
+def revert_stateful_config_changes(linter: PyLinter) -> Iterator[PyLinter]:
+    yield linter
+    # Revert any stateful configuration changes.
+    MANAGER.brain["module_denylist"] = set()
+
+
+@pytest.mark.usefixtures("revert_stateful_config_changes")
 @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES)
 def test_functional(test_file: FunctionalTestFile, pytestconfig: Config) -> None:
     __tracebackhide__ = True  # pylint: disable=unused-variable