Merge branch 'maintenance/4.0.x' into post-4.0.3
diff --git a/.coveragerc b/.coveragerc
index 55838fc..ca605f7 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,6 +3,7 @@
 
 [report]
 omit =
+    */astroid/__main__.py
     */tests/*
     */tmp*/*
 exclude_lines =
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 4d644f4..d627224 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -25,7 +25,7 @@
         )
       )
     steps:
-      - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
+      - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
         id: app-token
         with:
           app-id: ${{ vars.BACKPORT_APP_ID }}
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 98edfb7..4f17d84 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -26,11 +26,11 @@
     steps:
       - &checkout
         name: Check out code from GitHub
-        uses: actions/checkout@v5.0.0
+        uses: actions/checkout@v6.0.1
       - &setup-python-default
         name: Set up Python ${{ env.DEFAULT_PYTHON }}
         id: python
-        uses: actions/setup-python@v6.0.0
+        uses: actions/setup-python@v6.1.0
         with:
           python-version: ${{ env.DEFAULT_PYTHON }}
           check-latest: true
@@ -43,7 +43,7 @@
       - &cache-python
         name: Restore Python virtual environment
         id: cache-venv
-        uses: actions/cache@v4.3.0
+        uses: actions/cache@v5.0.1
         with:
           path: venv
           key: >-
@@ -63,7 +63,7 @@
             hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
       - name: Restore pre-commit environment
         id: cache-precommit
-        uses: actions/cache@v4.3.0
+        uses: actions/cache@v5.0.1
         with:
           path: ${{ env.PRE_COMMIT_CACHE }}
           key: >-
@@ -92,7 +92,7 @@
       - *checkout
       - name: Set up Python ${{ matrix.python-version }}
         id: python
-        uses: actions/setup-python@v6.0.0
+        uses: actions/setup-python@v6.1.0
         with:
           python-version: ${{ matrix.python-version }}
           allow-prereleases: true
@@ -122,7 +122,7 @@
           . venv/bin/activate
           pytest --cov
       - name: Upload coverage artifact
-        uses: &actions-upload-artifact actions/upload-artifact@v4.6.2
+        uses: &actions-upload-artifact actions/upload-artifact@v6.0.0
         with:
           name: coverage-linux-${{ matrix.python-version }}
           path: .coverage
@@ -146,7 +146,7 @@
       - &setup-python-matrix
         name: Set up Python ${{ matrix.python-version }}
         id: python
-        uses: actions/setup-python@v6.0.0
+        uses: actions/setup-python@v6.1.0
         with:
           python-version: ${{ matrix.python-version }}
           allow-prereleases: true
@@ -226,7 +226,7 @@
       - name: Install dependencies
         run: pip install -U -r requirements_minimal.txt
       - name: Download all coverage artifacts
-        uses: actions/download-artifact@v5.0.0
+        uses: actions/download-artifact@v7.0.0
       - name: Combine Linux coverage results
         run: |
           coverage combine coverage-linux*/.coverage
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 9b30ac2..f11f399 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -46,11 +46,11 @@
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v5.0.0
+        uses: actions/checkout@v6.0.1
 
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v3
+        uses: github/codeql-action/init@v4
         with:
           languages: ${{ matrix.language }}
           # If you wish to specify custom queries, you can do so here or in a config file.
@@ -61,7 +61,7 @@
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@v3
+        uses: github/codeql-action/autobuild@v4
 
       # â„šī¸ Command-line programs to run using the OS shell.
       # 📚 https://git.io/JvXDl
@@ -75,4 +75,4 @@
       #   make release
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v3
+        uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2c6cbdb..af5c9a5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,10 +18,10 @@
     if: github.event_name == 'release'
     steps:
       - name: Check out code from Github
-        uses: actions/checkout@v5.0.0
+        uses: actions/checkout@v6.0.1
       - name: Set up Python ${{ env.DEFAULT_PYTHON }}
         id: python
-        uses: actions/setup-python@v6.0.0
+        uses: actions/setup-python@v6.1.0
         with:
           python-version: ${{ env.DEFAULT_PYTHON }}
           check-latest: true
@@ -34,7 +34,7 @@
         run: |
           python -m build
       - name: Upload release assets
-        uses: actions/upload-artifact@v4.6.2
+        uses: actions/upload-artifact@v6.0.0
         with:
           name: release-assets
           path: dist/
@@ -50,7 +50,7 @@
       id-token: write
     steps:
       - name: Download release assets
-        uses: actions/download-artifact@v5.0.0
+        uses: actions/download-artifact@v7.0.0
         with:
           name: release-assets
           path: dist/
@@ -67,13 +67,13 @@
       id-token: write
     steps:
       - name: Download release assets
-        uses: actions/download-artifact@v5.0.0
+        uses: actions/download-artifact@v7.0.0
         with:
           name: release-assets
           path: dist/
       - name: Sign the dists with Sigstore and upload assets to Github release
         if: github.event_name == 'release'
-        uses: sigstore/gh-action-sigstore-python@v3.0.1
+        uses: sigstore/gh-action-sigstore-python@v3.2.0
         with:
           inputs: |
             ./dist/*.tar.gz
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 70095b1..6d2ef84 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,7 +10,7 @@
       - id: end-of-file-fixer
         exclude: tests/testdata
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: "v0.13.2"
+    rev: "v0.14.10"
     hooks:
       - id: ruff-check
         args: ["--fix"]
@@ -22,7 +22,7 @@
         exclude: tests/testdata|setup.py
         types: [python]
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.20.0
+    rev: v3.21.2
     hooks:
       - id: pyupgrade
         exclude: tests/testdata
@@ -32,8 +32,8 @@
     hooks:
       - id: black-disable-checker
         exclude: tests/test_nodes_lineno.py
-  - repo: https://github.com/psf/black
-    rev: 25.9.0
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 25.12.0
     hooks:
       - id: black
         args: [--safe, --quiet]
@@ -68,7 +68,7 @@
           ]
         stages: [manual]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.18.2
+    rev: v1.19.1
     hooks:
       - id: mypy
         language: python
@@ -76,11 +76,11 @@
         require_serial: true
         additional_dependencies: ["types-typed-ast"]
   - repo: https://github.com/rbubley/mirrors-prettier
-    rev: v3.6.2
+    rev: v3.7.4
     hooks:
       - id: prettier
         args: [--prose-wrap=always, --print-width=88]
   - repo: https://github.com/tox-dev/pyproject-fmt
-    rev: "v2.6.0"
+    rev: "v2.11.1"
     hooks:
       - id: pyproject-fmt
diff --git a/ChangeLog b/ChangeLog
index df6f72e..0e4312e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,35 @@
 ============================
 Release date: TBA
 
+* Add support for type constraints (`isinstance(x, y)`) in inference.
+
+  Closes pylint-dev/pylint#1162
+  Closes pylint-dev/pylint#4635
+  Closes pylint-dev/pylint#10469
+
+* Make `type.__new__()` raise clear errors instead of returning `None`
+
+* Move object dunder methods from ``FunctionModel`` to ``ObjectModel`` to make them
+  available on all object types, not just functions.
+
+  Closes #2742
+  Closes #2741
+  Closes pylint-dev/pylint#6094
+
+* ``lineno`` and ``end_lineno`` are now available on ``Arguments``.
+
+* Add helper to iterate over all annotations nodes of function arguments,
+  ``Arguments.get_annotations()``.
+
+  Refs #2860
+
+* Skip direct parent when determining the ``Decorator`` frame.
+
+  Refs pylint-dev/pylint#8425
+
+* Add simple command line interface for astroid to output generated AST.
+  Use with ``python -m astroid``.
+
 
 
 What's New in astroid 4.0.4?
@@ -133,7 +162,7 @@
 
   Closes #2672
 
-* Add basic support for ``ast.TemplateStr`` and ``ast.Interpolation``added in Python 3.14.
+* Add basic support for ``ast.TemplateStr`` and ``ast.Interpolation`` added in Python 3.14.
 
   Refs #2789
 
diff --git a/astroid/__main__.py b/astroid/__main__.py
new file mode 100644
index 0000000..90a40d6
--- /dev/null
+++ b/astroid/__main__.py
@@ -0,0 +1,54 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
+
+"""Command line interface for astroid."""
+
+from __future__ import annotations
+
+import sys
+from argparse import ArgumentParser, Namespace
+from collections.abc import Callable, Sequence
+from pathlib import Path
+from typing import cast
+
+import astroid
+
+
+class Arguments(Namespace):
+    func: Callable[[Arguments], int]
+
+
+class ASTParserArguments(Arguments):
+    file: str
+
+
+def parse_ast(args: ASTParserArguments) -> int:
+    if not ((file := Path(args.file)).is_file() and file.suffix in {".py", ".pyi"}):
+        print(f"error: '{file}' does not exist or isn't a Python file")
+        return 1
+
+    tree = astroid.parse(file.read_text(encoding="utf8"))
+    print(tree.repr_tree())
+    return 0
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+    argv = argv or sys.argv[1:]
+    parser = ArgumentParser(description="Command line interface for astroid")
+    subparsers = parser.add_subparsers()
+
+    ast_parser = subparsers.add_parser("ast", help="Print astroid AST")
+    ast_parser.set_defaults(func=parse_ast)
+    ast_parser.add_argument("file", metavar="FILE", help="File to parse")
+
+    args = cast(Arguments, parser.parse_args(argv))
+    if "func" not in args:
+        parser.print_help()
+        return 2
+
+    return args.func(args)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py
index feea93f..dfe71c3 100644
--- a/astroid/__pkginfo__.py
+++ b/astroid/__pkginfo__.py
@@ -2,5 +2,5 @@
 # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
 # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
 
-__version__ = "4.0.3"
+__version__ = "4.1.0-dev0"
 version = __version__
diff --git a/astroid/bases.py b/astroid/bases.py
index a029da6..7fb3da2 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -250,7 +250,9 @@
             values = self._proxied.instance_attr(name, context)
         except AttributeInferenceError as exc:
             if self.special_attributes and name in self.special_attributes:
-                return [self.special_attributes.lookup(name)]
+                special_attr = self.special_attributes.lookup(name)
+                if not isinstance(special_attr, nodes.Unknown):
+                    return [special_attr]
 
             if lookupclass:
                 # Class attributes not available through the instance
@@ -571,10 +573,14 @@
             raise InferenceError(context=context) from e
         if not isinstance(mcs, nodes.ClassDef):
             # Not a valid first argument.
-            return None
+            raise InferenceError(
+                "type.__new__() requires a class for metaclass", context=context
+            )
         if not mcs.is_subtype_of("builtins.type"):
             # Not a valid metaclass.
-            return None
+            raise InferenceError(
+                "type.__new__() metaclass must be a subclass of type", context=context
+            )
 
         # Verify the name
         try:
@@ -583,10 +589,14 @@
             raise InferenceError(context=context) from e
         if not isinstance(name, nodes.Const):
             # Not a valid name, needs to be a const.
-            return None
+            raise InferenceError(
+                "type.__new__() requires a constant for name", context=context
+            )
         if not isinstance(name.value, str):
             # Needs to be a string.
-            return None
+            raise InferenceError(
+                "type.__new__() requires a string for name", context=context
+            )
 
         # Verify the bases
         try:
@@ -595,14 +605,18 @@
             raise InferenceError(context=context) from e
         if not isinstance(bases, nodes.Tuple):
             # Needs to be a tuple.
-            return None
+            raise InferenceError(
+                "type.__new__() requires a tuple for bases", context=context
+            )
         try:
             inferred_bases = [next(elt.infer(context=context)) for elt in bases.elts]
         except StopIteration as e:
             raise InferenceError(context=context) from e
         if any(not isinstance(base, nodes.ClassDef) for base in inferred_bases):
             # All the bases needs to be Classes
-            return None
+            raise InferenceError(
+                "type.__new__() requires classes for bases", context=context
+            )
 
         # Verify the attributes.
         try:
@@ -611,7 +625,9 @@
             raise InferenceError(context=context) from e
         if not isinstance(attrs, nodes.Dict):
             # Needs to be a dictionary.
-            return None
+            raise InferenceError(
+                "type.__new__() requires a dict for attrs", context=context
+            )
         cls_locals: dict[str, list[InferenceResult]] = collections.defaultdict(list)
         for key, value in attrs.items:
             try:
@@ -664,9 +680,13 @@
             and self.bound.name == "type"
             and self.name == "__new__"
             and isinstance(caller, nodes.Call)
-            and len(caller.args) == 4
         ):
             # Check if we have a ``type.__new__(mcs, name, bases, attrs)`` call.
+            if len(caller.args) != 4:
+                raise InferenceError(
+                    f"type.__new__() requires 4 arguments, got {len(caller.args)}",
+                    context=context,
+                )
             new_cls = self._infer_type_new_call(caller, context)
             if new_cls:
                 return iter((new_cls,))
diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py
index e21d361..a2ca955 100644
--- a/astroid/brain/brain_builtin_inference.py
+++ b/astroid/brain/brain_builtin_inference.py
@@ -763,7 +763,7 @@
     # The right hand argument is the class(es) that the given
     # object is to be checked against.
     try:
-        class_container = _class_or_tuple_to_container(
+        class_container = helpers.class_or_tuple_to_container(
             class_or_tuple_node, context=context
         )
     except InferenceError as exc:
@@ -798,7 +798,7 @@
     # The right hand argument is the class(es) that the given
     # obj is to be check is an instance of
     try:
-        class_container = _class_or_tuple_to_container(
+        class_container = helpers.class_or_tuple_to_container(
             class_or_tuple_node, context=context
         )
     except InferenceError as exc:
@@ -814,30 +814,6 @@
     return nodes.Const(isinstance_bool)
 
 
-def _class_or_tuple_to_container(
-    node: InferenceResult, context: InferenceContext | None = None
-) -> list[InferenceResult]:
-    # Move inferences results into container
-    # to simplify later logic
-    # raises InferenceError if any of the inferences fall through
-    try:
-        node_infer = next(node.infer(context=context))
-    except StopIteration as e:
-        raise InferenceError(node=node, context=context) from e
-    # arg2 MUST be a type or a TUPLE of types
-    # for isinstance
-    if isinstance(node_infer, nodes.Tuple):
-        try:
-            class_container = [
-                next(node.infer(context=context)) for node in node_infer.elts
-            ]
-        except StopIteration as e:
-            raise InferenceError(node=node, context=context) from e
-    else:
-        class_container = [node_infer]
-    return class_container
-
-
 def infer_len(node, context: InferenceContext | None = None) -> nodes.Const:
     """Infer length calls.
 
diff --git a/astroid/constraint.py b/astroid/constraint.py
index 692d22d..693de59 100644
--- a/astroid/constraint.py
+++ b/astroid/constraint.py
@@ -10,7 +10,8 @@
 from collections.abc import Iterator
 from typing import TYPE_CHECKING
 
-from astroid import nodes, util
+from astroid import helpers, nodes, util
+from astroid.exceptions import AstroidTypeError, InferenceError, MroError
 from astroid.typing import InferenceResult
 
 if sys.version_info >= (3, 11):
@@ -77,7 +78,7 @@
     def satisfied_by(self, inferred: InferenceResult) -> bool:
         """Return True if this constraint is satisfied by the given inferred value."""
         # Assume true if uninferable
-        if isinstance(inferred, util.UninferableBase):
+        if inferred is util.Uninferable:
             return True
 
         # Return the XOR of self.negate and matches(inferred, self.CONST_NONE)
@@ -117,14 +118,61 @@
         - negate=True: satisfied if boolean value is False
         """
         inferred_booleaness = inferred.bool_value()
-        if isinstance(inferred, util.UninferableBase) or isinstance(
-            inferred_booleaness, util.UninferableBase
-        ):
+        if inferred is util.Uninferable or inferred_booleaness is util.Uninferable:
             return True
 
         return self.negate ^ inferred_booleaness
 
 
+class TypeConstraint(Constraint):
+    """Represents an "isinstance(x, y)" constraint."""
+
+    def __init__(
+        self, node: nodes.NodeNG, classinfo: nodes.NodeNG, negate: bool
+    ) -> None:
+        super().__init__(node=node, negate=negate)
+        self.classinfo = classinfo
+
+    @classmethod
+    def match(
+        cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
+    ) -> Self | None:
+        """Return a new constraint for node if expr matches the
+        "isinstance(x, y)" pattern. Else, return None.
+        """
+        is_instance_call = (
+            isinstance(expr, nodes.Call)
+            and isinstance(expr.func, nodes.Name)
+            and expr.func.name == "isinstance"
+            and not expr.keywords
+            and len(expr.args) == 2
+        )
+        if is_instance_call and _matches(expr.args[0], node):
+            return cls(node=node, classinfo=expr.args[1], negate=negate)
+
+        return None
+
+    def satisfied_by(self, inferred: InferenceResult) -> bool:
+        """Return True for uninferable results, or depending on negate flag:
+
+        - negate=False: satisfied when inferred is an instance of the checked types.
+        - negate=True: satisfied when inferred is not an instance of the checked types.
+        """
+        if inferred is util.Uninferable:
+            return True
+
+        try:
+            types = helpers.class_or_tuple_to_container(self.classinfo)
+            matches_checked_types = helpers.object_isinstance(inferred, types)
+
+            if matches_checked_types is util.Uninferable:
+                return True
+
+            return self.negate ^ matches_checked_types
+        except (InferenceError, AstroidTypeError, MroError):
+            return True
+
+
 def get_constraints(
     expr: _NameNodes, frame: nodes.LocalsDictNodeNG
 ) -> dict[nodes.If | nodes.IfExp, set[Constraint]]:
@@ -159,6 +207,7 @@
     (
         NoneConstraint,
         BooleanConstraint,
+        TypeConstraint,
     )
 )
 """All supported constraint types."""
diff --git a/astroid/helpers.py b/astroid/helpers.py
index 9c370aa..deef3d9 100644
--- a/astroid/helpers.py
+++ b/astroid/helpers.py
@@ -170,6 +170,30 @@
     return _object_type_is_subclass(node, class_or_seq, context=context)
 
 
+def class_or_tuple_to_container(
+    node: InferenceResult, context: InferenceContext | None = None
+) -> list[InferenceResult]:
+    # Move inferences results into container
+    # to simplify later logic
+    # raises InferenceError if any of the inferences fall through
+    try:
+        node_infer = next(node.infer(context=context))
+    except StopIteration as e:  # pragma: no cover
+        raise InferenceError(node=node, context=context) from e
+    # arg2 MUST be a type or a TUPLE of types
+    # for isinstance
+    if isinstance(node_infer, nodes.Tuple):
+        try:
+            class_container = [
+                next(node.infer(context=context)) for node in node_infer.elts
+            ]
+        except StopIteration as e:  # pragma: no cover
+            raise InferenceError(node=node, context=context) from e
+    else:
+        class_container = [node_infer]
+    return class_container
+
+
 def has_known_bases(klass, context: InferenceContext | None = None) -> bool:
     """Return whether all base classes of a class could be inferred."""
     try:
diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py
index eac9e43..3745107 100644
--- a/astroid/interpreter/objectmodel.py
+++ b/astroid/interpreter/objectmodel.py
@@ -163,6 +163,33 @@
 
         return bases.BoundMethod(proxy=node, bound=_get_bound_node(self))
 
+    # Base object attributes that return Unknown as fallback placeholders.
+    @property
+    def attr___ne__(self):
+        return node_classes.Unknown(parent=self._instance)
+
+    attr___class__ = attr___ne__
+    attr___delattr__ = attr___ne__
+    attr___dir__ = attr___ne__
+    attr___doc__ = attr___ne__
+    attr___eq__ = attr___ne__
+    attr___format__ = attr___ne__
+    attr___ge__ = attr___ne__
+    attr___getattribute__ = attr___ne__
+    attr___getstate__ = attr___ne__
+    attr___gt__ = attr___ne__
+    attr___hash__ = attr___ne__
+    attr___init_subclass__ = attr___ne__
+    attr___le__ = attr___ne__
+    attr___lt__ = attr___ne__
+    attr___reduce__ = attr___ne__
+    attr___reduce_ex__ = attr___ne__
+    attr___repr__ = attr___ne__
+    attr___setattr__ = attr___ne__
+    attr___sizeof__ = attr___ne__
+    attr___str__ = attr___ne__
+    attr___subclasshook__ = attr___ne__
+
 
 class ModuleModel(ObjectModel):
     def _builtins(self):
@@ -459,30 +486,15 @@
 
         return DescriptorBoundMethod(proxy=self._instance, bound=self._instance)
 
-    # These are here just for completion.
+    # Function-specific attributes.
     @property
-    def attr___ne__(self):
+    def attr___call__(self):
         return node_classes.Unknown(parent=self._instance)
 
-    attr___subclasshook__ = attr___ne__
-    attr___str__ = attr___ne__
-    attr___sizeof__ = attr___ne__
-    attr___setattr___ = attr___ne__
-    attr___repr__ = attr___ne__
-    attr___reduce__ = attr___ne__
-    attr___reduce_ex__ = attr___ne__
-    attr___lt__ = attr___ne__
-    attr___eq__ = attr___ne__
-    attr___gt__ = attr___ne__
-    attr___format__ = attr___ne__
-    attr___delattr___ = attr___ne__
-    attr___getattribute__ = attr___ne__
-    attr___hash__ = attr___ne__
-    attr___dir__ = attr___ne__
-    attr___call__ = attr___ne__
-    attr___class__ = attr___ne__
-    attr___closure__ = attr___ne__
-    attr___code__ = attr___ne__
+    attr___builtins__ = attr___call__
+    attr___closure__ = attr___call__
+    attr___code__ = attr___call__
+    attr___type_params__ = attr___call__
 
 
 class ClassModel(ObjectModel):
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index b9077de..7f0887b 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -55,6 +55,7 @@
 if TYPE_CHECKING:
     from astroid import nodes
     from astroid.nodes import LocalsDictNodeNG
+    from astroid.nodes.node_ng import FrameType
 
 
 def _is_const(value) -> bool:
@@ -1020,6 +1021,23 @@
             if elt is not None:
                 yield elt
 
+    def get_annotations(self) -> Iterator[nodes.NodeNG]:
+        """Iterate over all annotations nodes."""
+        for elt in self.posonlyargs_annotations:
+            if elt is not None:
+                yield elt
+        for elt in self.annotations:
+            if elt is not None:
+                yield elt
+        if self.varargannotation is not None:
+            yield self.varargannotation
+
+        for elt in self.kwonlyargs_annotations:
+            if elt is not None:
+                yield elt
+        if self.kwargannotation is not None:
+            yield self.kwargannotation
+
     @decorators.raise_if_nothing_inferred
     def _infer(
         self, context: InferenceContext | None = None, **kwargs: Any
@@ -2222,13 +2240,22 @@
 
         :returns: The first parent scope node.
         """
-        # skip the function node to go directly to the upper level scope
+        # skip the function or class node to go directly to the upper level scope
         if not self.parent:
             raise ParentMissingError(target=self)
         if not self.parent.parent:
             raise ParentMissingError(target=self.parent)
         return self.parent.parent.scope()
 
+    def frame(self) -> FrameType:
+        """The first parent node defining a new frame."""
+        # skip the function or class node to go directly to the upper level frame
+        if not self.parent:
+            raise ParentMissingError(target=self)
+        if not self.parent.parent:
+            raise ParentMissingError(target=self.parent)
+        return self.parent.parent.frame()
+
     def get_children(self):
         yield from self.nodes
 
@@ -4916,7 +4943,7 @@
     See astroid/protocols.py for actual implementation.
     """
 
-    def frame(self) -> nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda:
+    def frame(self) -> FrameType:
         """The first parent frame node.
 
         A frame node is a :class:`Module`, :class:`FunctionDef`,
@@ -4972,9 +4999,10 @@
 
 class Unknown(_base_nodes.AssignTypeNode):
     """This node represents a node in a constructed AST where
-    introspection is not possible.  At the moment, it's only used in
-    the args attribute of FunctionDef nodes where function signature
-    introspection failed.
+    introspection is not possible.
+
+    Used in the args attribute of FunctionDef nodes where function signature
+    introspection failed, and as a placeholder in ObjectModel.
     """
 
     name = "Unknown"
diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py
index 1af39c2..af40c68 100644
--- a/astroid/nodes/node_ng.py
+++ b/astroid/nodes/node_ng.py
@@ -42,6 +42,8 @@
 if TYPE_CHECKING:
     from astroid.nodes import _base_nodes
 
+    FrameType = nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda
+
 
 # Types for 'NodeNG.nodes_of_class()'
 _NodesT = TypeVar("_NodesT", bound="NodeNG")
@@ -284,7 +286,7 @@
             raise StatementMissing(target=self)
         return self.parent.statement()
 
-    def frame(self) -> nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda:
+    def frame(self) -> FrameType:
         """The first parent frame node.
 
         A frame node is a :class:`Module`, :class:`FunctionDef`,
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index f9b06bf..744970f 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -1624,6 +1624,16 @@
                     yield node_classes.Const(None)
                 return
 
+            # Builtin dunder methods have empty bodies, return Uninferable.
+            if (
+                len(self.body) == 0
+                and self.name.startswith("__")
+                and self.name.endswith("__")
+                and self.root().qname() == "builtins"
+            ):
+                yield util.Uninferable
+                return
+
             raise InferenceError("The function does not have any return statements")
 
         for returnnode in itertools.chain((first_return,), returns):
@@ -2352,8 +2362,10 @@
             values += classnode.locals.get(name, [])
 
         if name in self.special_attributes and class_context and not values:
-            result = [self.special_attributes.lookup(name)]
-            return result
+            special_attr = self.special_attributes.lookup(name)
+            if not isinstance(special_attr, node_classes.Unknown):
+                result = [special_attr]
+                return result
 
         if class_context:
             values += self._metaclass_lookup_attribute(name, context)
diff --git a/astroid/raw_building.py b/astroid/raw_building.py
index d1bbbd5..e7c5562 100644
--- a/astroid/raw_building.py
+++ b/astroid/raw_building.py
@@ -78,7 +78,11 @@
     """create a Const node and register it in the locals of the given
     node with the specified name
     """
-    if name not in node.special_attributes:
+    # Special case: __hash__ = None overrides ObjectModel for unhashable types.
+    # See https://docs.python.org/3/reference/datamodel.html#object.__hash__
+    if name == "__hash__" and value is None:
+        _attach_local_node(node, nodes.const_factory(value), name)
+    elif name not in node.special_attributes:
         _attach_local_node(node, nodes.const_factory(value), name)
 
 
@@ -507,7 +511,11 @@
             elif inspect.isdatadescriptor(member):
                 child = object_build_datadescriptor(node, member)
             elif isinstance(member, tuple(node_classes.CONST_CLS)):
-                if alias in node.special_attributes:
+                # Special case: __hash__ = None overrides ObjectModel for unhashable types.
+                # See https://docs.python.org/3/reference/datamodel.html#object.__hash__
+                if alias in node.special_attributes and not (
+                    alias == "__hash__" and member is None
+                ):
                     continue
                 child = nodes.const_factory(member)
             elif inspect.isroutine(member):
diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py
index 97f3a39..f7d38a9 100644
--- a/astroid/rebuilder.py
+++ b/astroid/rebuilder.py
@@ -9,6 +9,7 @@
 from __future__ import annotations
 
 import ast
+import itertools
 import sys
 import token
 from collections.abc import Callable, Collection, Generator
@@ -589,6 +590,16 @@
             type_comment_kwonlyargs=type_comment_kwonlyargs,
             type_comment_posonlyargs=type_comment_posonlyargs,
         )
+        if start_end_lineno_pairs := [
+            (arg.lineno, arg.end_lineno)
+            for arg in itertools.chain(
+                node.args, node.posonlyargs, node.kwonlyargs, [node.vararg, node.kwarg]
+            )
+            if arg
+        ]:
+            newnode.lineno = min(startend[0] for startend in start_end_lineno_pairs)
+            newnode.end_lineno = max(startend[1] for startend in start_end_lineno_pairs)
+
         # save argument names in locals:
         assert newnode.parent
         if vararg:
diff --git a/doc/requirements.txt b/doc/requirements.txt
index cf9e808..a759a74 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,3 +1,3 @@
 -e .
 sphinx~=8.1
-furo==2025.9.25
+furo==2025.12.19
diff --git a/requirements_minimal.txt b/requirements_minimal.txt
index 9fde495..c1457cf 100644
--- a/requirements_minimal.txt
+++ b/requirements_minimal.txt
@@ -3,7 +3,7 @@
 tbump~=6.11
 
 # Tools used to run tests
-coverage~=7.10
+coverage~=7.13
 pytest
 pytest-cov~=7.0
 mypy; platform_python_implementation!="PyPy"
diff --git a/tbump.toml b/tbump.toml
index 29eba19..42dcf88 100644
--- a/tbump.toml
+++ b/tbump.toml
@@ -1,7 +1,7 @@
 github_url = "https://github.com/pylint-dev/astroid"
 
 [version]
-current = "4.0.3"
+current = "4.1.0-dev0"
 regex = '''
 ^(?P<major>0|[1-9]\d*)
 \.
diff --git a/tests/test_constraint.py b/tests/test_constraint.py
index 4859d42..f69e0e4 100644
--- a/tests/test_constraint.py
+++ b/tests/test_constraint.py
@@ -5,9 +5,12 @@
 """Tests for inference involving constraints."""
 from __future__ import annotations
 
+from unittest.mock import patch
+
 import pytest
 
 from astroid import builder, nodes
+from astroid.bases import Instance
 from astroid.util import Uninferable
 
 
@@ -19,6 +22,8 @@
             (f"{node} is not None", 3, None),
             (f"{node}", 3, None),
             (f"not {node}", None, 3),
+            (f"isinstance({node}, int)", 3, None),
+            (f"isinstance({node}, (int, str))", 3, None),
         ),
     )
 
@@ -773,3 +778,295 @@
     assert isinstance(inferred[0], nodes.Const)
     assert inferred[0].value == fail_val
     assert inferred[1].value is Uninferable
+
+
+def test_isinstance_equal_types() -> None:
+    """Test constraint for an object whose type is equal to the checked type."""
+    node = builder.extract_node(
+        """
+    class A:
+        pass
+
+    x = A()
+
+    if isinstance(x, A):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], Instance)
+    assert isinstance(inferred[0]._proxied, nodes.ClassDef)
+    assert inferred[0].name == "A"
+
+
+def test_isinstance_subtype() -> None:
+    """Test constraint for an object whose type is a strict subtype of the checked type."""
+    node = builder.extract_node(
+        """
+    class A:
+        pass
+
+    class B(A):
+        pass
+
+    x = B()
+
+    if isinstance(x, A):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], Instance)
+    assert isinstance(inferred[0]._proxied, nodes.ClassDef)
+    assert inferred[0].name == "B"
+
+
+def test_isinstance_unrelated_types():
+    """Test constraint for an object whose type is not related to the checked type."""
+    node = builder.extract_node(
+        """
+    class A:
+        pass
+
+    class B:
+        pass
+
+    x = A()
+
+    if isinstance(x, B):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_isinstance_supertype():
+    """Test constraint for an object whose type is a strict supertype of the checked type."""
+    node = builder.extract_node(
+        """
+    class A:
+        pass
+
+    class B(A):
+        pass
+
+    x = A()
+
+    if isinstance(x, B):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_isinstance_multiple_inheritance():
+    """Test constraint for an object that inherits from more than one parent class."""
+    n1, n2, n3 = builder.extract_node(
+        """
+    class A:
+        pass
+
+    class B:
+        pass
+
+    class C(A, B):
+        pass
+
+    x = C()
+
+    if isinstance(x, C):
+        x  #@
+
+    if isinstance(x, A):
+        x  #@
+
+    if isinstance(x, B):
+        x  #@
+    """
+    )
+
+    for node in (n1, n2, n3):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], Instance)
+        assert isinstance(inferred[0]._proxied, nodes.ClassDef)
+        assert inferred[0].name == "C"
+
+
+def test_isinstance_diamond_inheritance():
+    """Test constraint for an object that inherits from parent classes
+    in diamond inheritance.
+    """
+    n1, n2, n3, n4 = builder.extract_node(
+        """
+    class A():
+        pass
+
+    class B(A):
+        pass
+
+    class C(A):
+        pass
+
+    class D(B, C):
+        pass
+
+    x = D()
+
+    if isinstance(x, D):
+        x  #@
+
+    if isinstance(x, B):
+        x  #@
+
+    if isinstance(x, C):
+        x  #@
+
+    if isinstance(x, A):
+        x  #@
+    """
+    )
+
+    for node in (n1, n2, n3, n4):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], Instance)
+        assert isinstance(inferred[0]._proxied, nodes.ClassDef)
+        assert inferred[0].name == "D"
+
+
+def test_isinstance_keyword_arguments():
+    """Test that constraint does not apply when `isinstance` is called
+    with keyword arguments.
+    """
+    n1, n2 = builder.extract_node(
+        """
+    x = 3
+
+    if isinstance(object=x, classinfo=str):
+        x  #@
+
+    if isinstance(x, str, object=x, classinfo=str):
+        x  #@
+    """
+    )
+
+    for node in (n1, n2):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.Const)
+        assert inferred[0].value == 3
+
+
+def test_isinstance_extra_argument():
+    """Test that constraint does not apply when `isinstance` is called
+    with more than two positional arguments.
+    """
+    node = builder.extract_node(
+        """
+    x = 3
+
+    if isinstance(x, str, bool):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 3
+
+
+def test_isinstance_classinfo_inference_error():
+    """Test that constraint is satisfied when `isinstance` is called with
+    classinfo that raises an inference error.
+    """
+    node = builder.extract_node(
+        """
+    x = 3
+
+    if isinstance(x, undefined_type):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 3
+
+
+def test_isinstance_uninferable_classinfo():
+    """Test that constraint is satisfied when `isinstance` is called with
+    uninferable classinfo.
+    """
+    node = builder.extract_node(
+        """
+    def f(classinfo):
+        x = 3
+
+        if isinstance(x, classinfo):
+            x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 3
+
+
+def test_isinstance_mro_error():
+    """Test that constraint is satisfied when computing the object's
+    method resolution order raises an MRO error.
+    """
+    node = builder.extract_node(
+        """
+    class A():
+        pass
+
+    class B(A, A):
+        pass
+
+    x = B()
+
+    if isinstance(x, A):
+        x  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], Instance)
+    assert isinstance(inferred[0]._proxied, nodes.ClassDef)
+    assert inferred[0].name == "B"
+
+
+def test_isinstance_uninferable():
+    """Test that constraint is satisfied when `isinstance` inference returns Uninferable."""
+    node = builder.extract_node(
+        """
+    x = 3
+
+    if isinstance(x, str):
+        x  #@
+    """
+    )
+
+    with patch(
+        "astroid.constraint.helpers.object_isinstance", return_value=Uninferable
+    ):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.Const)
+        assert inferred[0].value == 3
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 4df145b..65d9780 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -10,7 +10,7 @@
 from astroid import builder, helpers, manager, nodes, raw_building, util
 from astroid.builder import AstroidBuilder
 from astroid.const import IS_PYPY
-from astroid.exceptions import _NonDeducibleTypeHierarchy
+from astroid.exceptions import InferenceError, _NonDeducibleTypeHierarchy
 from astroid.nodes.node_classes import UNATTACHED_UNKNOWN
 
 
@@ -275,3 +275,70 @@
         "Import safe_infer from astroid.util; this shim in astroid.helpers will be removed."
         in records[0].message.args[0]
     )
+
+
+def test_class_to_container() -> None:
+    node = builder.extract_node("""isinstance(3, int)""")
+
+    container = helpers.class_or_tuple_to_container(node.args[1])
+
+    assert len(container) == 1
+    assert isinstance(container[0], nodes.ClassDef)
+    assert container[0].name == "int"
+
+
+def test_tuple_to_container() -> None:
+    node = builder.extract_node("""isinstance(3, (int, str))""")
+
+    container = helpers.class_or_tuple_to_container(node.args[1])
+
+    assert len(container) == 2
+
+    assert isinstance(container[0], nodes.ClassDef)
+    assert container[0].name == "int"
+
+    assert isinstance(container[1], nodes.ClassDef)
+    assert container[1].name == "str"
+
+
+def test_class_to_container_uninferable() -> None:
+    node = builder.extract_node(
+        """
+    def f(x):
+        isinstance(3, x)  #@
+    """
+    )
+
+    container = helpers.class_or_tuple_to_container(node.args[1])
+
+    assert len(container) == 1
+    assert container[0] is util.Uninferable
+
+
+def test_tuple_to_container_uninferable() -> None:
+    node = builder.extract_node(
+        """
+    def f(x, y):
+        isinstance(3, (x, y))  #@
+    """
+    )
+
+    container = helpers.class_or_tuple_to_container(node.args[1])
+
+    assert len(container) == 2
+    assert container[0] is util.Uninferable
+    assert container[1] is util.Uninferable
+
+
+def test_class_to_container_inference_error() -> None:
+    node = builder.extract_node("""isinstance(3, undefined_type)""")
+
+    with pytest.raises(InferenceError):
+        helpers.class_or_tuple_to_container(node.args[1])
+
+
+def test_tuple_to_container_inference_error() -> None:
+    node = builder.extract_node("""isinstance(3, (int, undefined_type))""")
+
+    with pytest.raises(InferenceError):
+        helpers.class_or_tuple_to_container(node.args[1])
diff --git a/tests/test_inference.py b/tests/test_inference.py
index 6a19220..e0d47a7 100644
--- a/tests/test_inference.py
+++ b/tests/test_inference.py
@@ -1503,7 +1503,7 @@
 
     def test_python25_no_relative_import(self) -> None:
         ast = resources.build_file("data/package/absimport.py")
-        self.assertTrue(ast.absolute_import_activated(), True)
+        self.assertTrue(ast.absolute_import_activated())
         inferred = next(
             test_utils.get_name_node(ast, "import_package_subpackage_module").infer()
         )
@@ -1512,7 +1512,7 @@
 
     def test_nonregr_absolute_import(self) -> None:
         ast = resources.build_file("data/absimp/string.py", "data.absimp.string")
-        self.assertTrue(ast.absolute_import_activated(), True)
+        self.assertTrue(ast.absolute_import_activated())
         inferred = next(test_utils.get_name_node(ast, "string").infer())
         self.assertIsInstance(inferred, nodes.Module)
         self.assertEqual(inferred.name, "string")
@@ -5618,8 +5618,9 @@
     )
     inferred = next(node.infer())
     lenmeth = next(inferred.igetattr("__len__"))
-    with pytest.raises(InferenceError):
-        next(lenmeth.infer_call_result(None, None))
+    # Builtin dunder methods now return Uninferable instead of raising InferenceError
+    result = next(lenmeth.infer_call_result(None, None))
+    assert result is util.Uninferable
 
 
 def test_unpack_dicts_in_assignment() -> None:
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index 8d2f4d5..939c581 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -2314,6 +2314,19 @@
     assert node.args.default_value("flavor").value == "good"
 
 
+def test_arguments_annotations():
+    node = extract_node(
+        "def fruit(eat: str, /, peel: bool, *args: int, trim: float, **kwargs: bytes): ..."
+    )
+    assert isinstance(node.args, nodes.Arguments)
+    annotation_names = [
+        ann.name for ann in node.args.get_annotations() if isinstance(ann, nodes.Name)
+    ]
+    assert all(
+        name in annotation_names for name in ("str", "bool", "int", "float", "bytes")
+    )
+
+
 def test_deprecated_nodes_import_from_toplevel():
     # pylint: disable=import-outside-toplevel,no-name-in-module
     with pytest.raises(
diff --git a/tests/test_nodes_lineno.py b/tests/test_nodes_lineno.py
index f8c6f91..25fb339 100644
--- a/tests/test_nodes_lineno.py
+++ b/tests/test_nodes_lineno.py
@@ -589,6 +589,8 @@
         assert (f1.body[0].lineno, f1.body[0].col_offset) == (6, 4)
         assert (f1.body[0].end_lineno, f1.body[0].end_col_offset) == (6, 8)
 
+        assert (f1.args.lineno, f1.args.end_lineno) == (2, 4)
+
         # pos only arguments
         # TODO fix column offset: arg -> arg (AssignName)
         assert isinstance(f1.args.posonlyargs[0], nodes.AssignName)
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index f3015db..f36a0d5 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -265,10 +265,13 @@
         xml.__setattr__ #@
         xml.__reduce_ex__ #@
         xml.__lt__ #@
+        xml.__le__ #@
         xml.__eq__ #@
+        xml.__ne__ #@
+        xml.__ge__ #@
         xml.__gt__ #@
         xml.__format__ #@
-        xml.__delattr___ #@
+        xml.__delattr__ #@
         xml.__getattribute__ #@
         xml.__hash__ #@
         xml.__dir__ #@
@@ -324,9 +327,13 @@
         new_ = next(ast_nodes[10].infer())
         assert isinstance(new_, bases.BoundMethod)
 
-        # The following nodes are just here for theoretical completeness,
-        # and they either return Uninferable or raise InferenceError.
-        for ast_node in ast_nodes[11:28]:
+        # Inherited attributes return Uninferable.
+        for ast_node in ast_nodes[11:29]:
+            inferred = next(ast_node.infer())
+            self.assertIs(inferred, astroid.Uninferable)
+
+        # Attributes that don't exist on modules raise InferenceError.
+        for ast_node in ast_nodes[29:31]:
             with pytest.raises(InferenceError):
                 next(ast_node.infer())
 
@@ -449,16 +456,23 @@
 
         func.__reduce_ex__ #@
         func.__lt__ #@
+        func.__le__ #@
         func.__eq__ #@
+        func.__ne__ #@
+        func.__ge__ #@
         func.__gt__ #@
         func.__format__ #@
-        func.__delattr___ #@
+        func.__delattr__ #@
         func.__getattribute__ #@
         func.__hash__ #@
         func.__dir__ #@
         func.__class__ #@
 
         func.__setattr__ #@
+        func.__builtins__ #@
+        func.__getstate__ #@
+        func.__init_subclass__ #@
+        func.__type_params__ #@
         ''',
             module_name="fake_module",
         )
@@ -511,16 +525,11 @@
         new_ = next(ast_nodes[10].infer())
         assert isinstance(new_, bases.BoundMethod)
 
-        # The following nodes are just here for theoretical completeness,
-        # and they either return Uninferable or raise InferenceError.
-        for ast_node in ast_nodes[11:26]:
+        # Remaining attributes return Uninferable.
+        for ast_node in ast_nodes[11:34]:
             inferred = next(ast_node.infer())
             assert inferred is util.Uninferable
 
-        for ast_node in ast_nodes[26:27]:
-            with pytest.raises(InferenceError):
-                inferred = next(ast_node.infer())
-
     def test_empty_return_annotation(self) -> None:
         ast_node = builder.extract_node(
             """
@@ -897,3 +906,112 @@
     assert next(apple.infer()) is astroid.Uninferable
     assert isinstance(pear, nodes.Attribute)
     assert next(pear.infer()) is astroid.Uninferable
+
+
+def test_object_dunder_methods_can_be_overridden() -> None:
+    """Test that ObjectModel dunders don't block class overrides."""
+    # Test instance method override
+    eq_result = builder.extract_node(
+        """
+        class MyClass:
+            def __eq__(self, other):
+                return "custom equality"
+
+        MyClass().__eq__(None)  #@
+        """
+    )
+    inferred = next(eq_result.infer())
+    assert isinstance(inferred, nodes.Const)
+    assert inferred.value == "custom equality"
+
+    # Test that __eq__ on instance returns a bound method
+    eq_method = builder.extract_node(
+        """
+        class MyClass:
+            def __eq__(self, other):
+                return True
+
+        MyClass().__eq__  #@
+        """
+    )
+    inferred = next(eq_method.infer())
+    assert isinstance(inferred, astroid.BoundMethod)
+
+    # Test other commonly overridden dunders
+    for dunder, return_val in (
+        ("__ne__", "not equal"),
+        ("__lt__", "less than"),
+        ("__le__", "less or equal"),
+        ("__gt__", "greater than"),
+        ("__ge__", "greater or equal"),
+        ("__str__", "string repr"),
+        ("__repr__", "repr"),
+        ("__hash__", 42),
+    ):
+        node = builder.extract_node(
+            f"""
+            class MyClass:
+                def {dunder}(self, *args):
+                    return {return_val!r}
+
+            MyClass().{dunder}()  #@
+            """
+        )
+        inferred = next(node.infer())
+        assert isinstance(inferred, nodes.Const), f"{dunder} failed to infer correctly"
+        assert inferred.value == return_val, f"{dunder} returned wrong value"
+
+
+def test_unoverridden_object_dunders_return_uninferable() -> None:
+    """Test that un-overridden object dunders return Uninferable when called."""
+    for dunder in (
+        "__eq__",
+        "__hash__",
+        "__lt__",
+        "__le__",
+        "__gt__",
+        "__ge__",
+        "__ne__",
+    ):
+        node = builder.extract_node(
+            f"""
+            class MyClass:
+                pass
+
+            MyClass().{dunder}(None) if "{dunder}" != "__hash__" else MyClass().{dunder}()  #@
+            """
+        )
+        result = next(node.infer())
+        assert result is util.Uninferable
+
+
+def test_all_object_dunders_accessible() -> None:
+    """Test that object dunders are accessible on classes and instances."""
+    # Use actual dunders from object in the current Python version
+    object_dunders = [attr for attr in dir(object) if attr.startswith("__")]
+
+    cls, instance = builder.extract_node(
+        """
+        class MyClass:
+            pass
+
+        MyClass  #@
+        MyClass()  #@
+        """
+    )
+    cls = next(cls.infer())
+    instance = next(instance.infer())
+
+    for dunder in object_dunders:
+        assert cls.getattr(dunder)
+        assert instance.getattr(dunder)
+
+
+def test_hash_none_for_unhashable_builtins() -> None:
+    """Test that unhashable builtin types have __hash__ = None."""
+    for type_name in ("list", "dict", "set"):
+        node = builder.extract_node(f"{type_name}  #@")
+        cls = next(node.infer())
+        hash_attr = cls.getattr("__hash__")[0]
+        assert isinstance(hash_attr, nodes.Const)
+        assert hash_attr.value is None
diff --git a/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py
index f3ac0a6..85aebab 100644
--- a/tests/test_scoped_nodes.py
+++ b/tests/test_scoped_nodes.py
@@ -2938,6 +2938,39 @@
         assert module.frame() == module
 
     @staticmethod
+    def test_frame_node_for_decorators():
+        code = builder.extract_node(
+            """
+            def deco(var):
+                def inner(arg):
+                    ...
+                return inner
+
+            @deco(
+                x := 1  #@
+            )
+            def func():  #@
+                ...
+
+            @deco(
+                y := 2  #@
+            )
+            class A:  #@
+                ...
+        """
+        )
+        name_expr_node1, func_node, name_expr_node2, class_node = code
+        module = func_node.root()
+        assert name_expr_node1.scope() == module
+        assert name_expr_node1.frame() == module
+        assert name_expr_node2.scope() == module
+        assert name_expr_node2.frame() == module
+        assert module.locals.get("x") == [name_expr_node1.target]
+        assert module.locals.get("y") == [name_expr_node2.target]
+        assert "x" not in func_node.locals
+        assert "y" not in class_node.locals
+
+    @staticmethod
     def test_non_frame_node():
         """Test if the frame of non frame nodes is set correctly."""
         module = builder.parse(