Repository: pylint-dev/astroid

Issue:
dict.copy() result is not inferred as dict
### Steps to reproduce

Run this code:
```python
import astroid

code = """
x = dict()
y = x.copy()
"""

ast = astroid.parse(code)

for name in ('x', 'y'):
  node = ast.locals[name][0]
  print(name, node.inferred())
```

### Current behavior
x [<Dict.dict l.2 at 0x7fffe852e110>]
y [Uninferable]

### Expected behavior
I expect `y` to be inferred as `dict` too.

### `python -c "from astroid import __pkginfo__; print(__pkginfo__.version)"` output
2.4.2


Discussion / hints:
I think the same issue applies to `list`, `set` and `bytearray`. I think we can deal with all these at the same time.



Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index a418b1b7af..cf1004721e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -20,6 +20,10 @@ Release date: TBA
 * Rename ``ModuleSpec`` -> ``module_type`` constructor parameter to match attribute
   name and improve typing. Use ``type`` instead.
 
+* Infer the return value of the ``.copy()`` method on ``dict``, ``list``, ``set``,
+  and ``frozenset``.
+
+  Closes #1403
 
 What's New in astroid 2.11.6?
 =============================
diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py
index 5d7040a4e1..125e0858c8 100644
--- a/astroid/brain/brain_builtin_inference.py
+++ b/astroid/brain/brain_builtin_inference.py
@@ -4,8 +4,9 @@
 
 """Astroid hooks for various builtins."""
 
+import itertools
 from functools import partial
-from typing import Optional
+from typing import Iterator, Optional
 
 from astroid import arguments, helpers, inference_tip, nodes, objects, util
 from astroid.builder import AstroidBuilder
@@ -892,6 +893,22 @@ def _build_dict_with_elements(elements):
     return _build_dict_with_elements([])
 
 
+def _infer_copy_method(
+    node: nodes.Call, context: Optional[InferenceContext] = None
+) -> Iterator[nodes.NodeNG]:
+    assert isinstance(node.func, nodes.Attribute)
+    inferred_orig, inferred_copy = itertools.tee(node.func.expr.infer(context=context))
+    if all(
+        isinstance(
+            inferred_node, (nodes.Dict, nodes.List, nodes.Set, objects.FrozenSet)
+        )
+        for inferred_node in inferred_orig
+    ):
+        return inferred_copy
+
+    raise UseInferenceDefault()
+
+
 # Builtins inference
 register_builtin_transform(infer_bool, "bool")
 register_builtin_transform(infer_super, "super")
@@ -920,3 +937,10 @@ def _build_dict_with_elements(elements):
     inference_tip(_infer_object__new__decorator),
     _infer_object__new__decorator_check,
 )
+
+AstroidManager().register_transform(
+    nodes.Call,
+    inference_tip(_infer_copy_method),
+    lambda node: isinstance(node.func, nodes.Attribute)
+    and node.func.attrname == "copy",
+)


Test changes:
diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py
index 4069d0f320..5b6f09b283 100644
--- a/tests/unittest_inference.py
+++ b/tests/unittest_inference.py
@@ -2051,6 +2051,37 @@ def test_dict_invalid_args(self) -> None:
             self.assertIsInstance(inferred, Instance)
             self.assertEqual(inferred.qname(), "builtins.dict")
 
+    def test_copy_method_inference(self) -> None:
+        code = """
+        a_dict = {"b": 1, "c": 2}
+        b_dict = a_dict.copy()
+        b_dict #@
+
+        a_list = [1, 2, 3]
+        b_list = a_list.copy()
+        b_list #@
+
+        a_set = set([1, 2, 3])
+        b_set = a_set.copy()
+        b_set #@
+
+        a_frozenset = frozenset([1, 2, 3])
+        b_frozenset = a_frozenset.copy()
+        b_frozenset #@
+
+        a_unknown = unknown()
+        b_unknown = a_unknown.copy()
+        b_unknown #@
+        """
+        ast = extract_node(code, __name__)
+        self.assertInferDict(ast[0], {"b": 1, "c": 2})
+        self.assertInferList(ast[1], [1, 2, 3])
+        self.assertInferSet(ast[2], [1, 2, 3])
+        self.assertInferFrozenSet(ast[3], [1, 2, 3])
+
+        inferred_unknown = next(ast[4].infer())
+        assert inferred_unknown == util.Uninferable
+
     def test_str_methods(self) -> None:
         code = """
         ' '.decode() #@