Repository: cython/cython

Issue:
[BUG] dict has been shared when unpacking it in lambda for default dict
<!--
**PLEASE READ THIS FIRST:**
- Do not use the bug and feature tracker for support requests. Use the `cython-users` mailing list instead.
- Did you search for similar issues already? Please do, it helps to save us precious time that we otherwise could not invest into development.
- Did you try the latest master branch or pre-release? It might already have what you want to report. Also see the [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) regarding recent changes.
-->

**To Reproduce**
Code to reproduce the behaviour:
```cython
import collections

def just_call():
    defaults = {'a': 0, 'b':0}

    data = collections.defaultdict(lambda: {**defaults})

    for x in range(5):
        if x in (1, 3):
            value = 1
        else:
            value = 4

        data[x]['a'] += value
        data[x]['b'] += value

    print(data)
    return data
```

```
In [1]: import test_default

In [2]: test_default.just_call()
defaultdict(<cyfunction just_call.<locals>.<lambda> at 0x7f588cf9f9a8>, {0: {'a': 14, 'b': 14}, 1: {'a': 14, 'b': 14}, 2: {'a': 14, 'b': 14}, 3: {'a': 14, 'b': 14}, 4: {'a': 14, 'b': 14}})
Out[2]:
defaultdict(<cyfunction just_call.<locals>.<lambda> at 0x7f588cf9f9a8>,
            {0: {'a': 14, 'b': 14},
             1: {'a': 14, 'b': 14},
             2: {'a': 14, 'b': 14},
             3: {'a': 14, 'b': 14},
             4: {'a': 14, 'b': 14}})
```

**Expected behavior**

```
In [1]: import test_default

In [2]: test_default.just_call()
defaultdict(<cyfunction just_call.<locals>.<lambda> at 0x7f98515d3a70>, {0: {'a': 4, 'b': 4}, 1: {'a': 1, 'b': 1}, 2: {'a': 4, 'b': 4}, 3: {'a': 1, 'b': 1}, 4: {'a': 4, 'b': 4}})
Out[2]:
defaultdict(<cyfunction just_call.<locals>.<lambda> at 0x7f98515d3a70>,
            {0: {'a': 4, 'b': 4},
             1: {'a': 1, 'b': 1},
             2: {'a': 4, 'b': 4},
             3: {'a': 1, 'b': 1},
             4: {'a': 4, 'b': 4}})
```

**Environment (please complete the following information):**
 - OS: [Linux]
 - Python version [e.g. 3.6.8]
 - Cython version [e.g. 0.29.21]



Discussion:
Duplicate of https://github.com/cython/cython/issues/3227

Thanks for the report. It does look like this is probably a bug

Resolving patch:
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py
--- a/Cython/Compiler/ExprNodes.py
+++ b/Cython/Compiler/ExprNodes.py
@@ -6754,22 +6754,13 @@ def infer_type(self, env):
         return dict_type
 
     def analyse_types(self, env):
-        args = [
+        self.keyword_args = [
             arg.analyse_types(env).coerce_to_pyobject(env).as_none_safe_node(
                 # FIXME: CPython's error message starts with the runtime function name
                 'argument after ** must be a mapping, not NoneType')
             for arg in self.keyword_args
         ]
 
-        if len(args) == 1 and args[0].type is dict_type:
-            # strip this intermediate node and use the bare dict
-            arg = args[0]
-            if arg.is_name and arg.entry.is_arg and len(arg.entry.cf_assignments) == 1:
-                # passing **kwargs through to function call => allow NULL
-                arg.allow_null = True
-            return arg
-
-        self.keyword_args = args
         return self
 
     def may_be_none(self):


Test patch:
diff --git a/tests/run/dict.pyx b/tests/run/dict.pyx
--- a/tests/run/dict.pyx
+++ b/tests/run/dict.pyx
@@ -117,3 +117,22 @@ def item_creation_sideeffect(L, sideeffect, unhashable):
     [2, 4, 5]
     """
     return {1:2, sideeffect(2): 3, 3: 4, unhashable(4): 5, sideeffect(5): 6}
+
+
+def dict_unpacking_not_for_arg_create_a_copy():
+    """
+    >>> dict_unpacking_not_for_arg_create_a_copy()
+    [('a', 'modified'), ('b', 'original')]
+    [('a', 'original'), ('b', 'original')]
+    """
+    data = {'a': 'original', 'b': 'original'}
+
+    func = lambda: {**data}
+
+    call_once = func()
+    call_once['a'] = 'modified'
+
+    call_twice = func()
+
+    print(sorted(call_once.items()))
+    print(sorted(call_twice.items()))
diff --git a/tests/run/kwargs_passthrough.pyx b/tests/run/kwargs_passthrough.pyx
--- a/tests/run/kwargs_passthrough.pyx
+++ b/tests/run/kwargs_passthrough.pyx
@@ -1,7 +1,6 @@
-cimport cython
+import cython
 
-
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def wrap_passthrough(f):
     """
     >>> def f(a=1): return a
@@ -80,7 +79,7 @@ def wrap_passthrough_more(f):
     return wrapper
 
 
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def wrap_passthrough2(f):
     """
     >>> def f(a=1): return a
@@ -99,7 +98,7 @@ def wrap_passthrough2(f):
     return wrapper
 
 
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def wrap_modify(f):
     """
     >>> def f(a=1, test=2):
@@ -123,7 +122,7 @@ def wrap_modify(f):
     return wrapper
 
 
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def wrap_modify_mix(f):
     """
     >>> def f(a=1, test=2):
@@ -187,7 +186,21 @@ def wrap_modify_func(f):
     return wrapper
 
 
-@cython.test_assert_path_exists('//MergedDictNode')
+def modify_in_function():
+    """
+    >>> modify_in_function()
+    {'foo': 'bar'}
+    {'foo': 'bar'}
+    """
+    def inner(**kwds):
+        kwds['foo'] = 'modified'
+    d = {'foo': 'bar'}
+    print(d)
+    inner(**d)
+    print(d)
+
+
+#@cython.test_assert_path_exists('//MergedDictNode')
 def wrap_modify_func_mix(f):
     """
     >>> def f(a=1, test=2):
@@ -215,12 +228,11 @@ def wrap_modify_func_mix(f):
     return wrapper
 
 
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def wrap_reassign(f):
     """
     >>> def f(a=1, test=2):
     ...     return a, test
-
     >>> wrapped = wrap_reassign(f)
     >>> wrapped(1)
     CALLED
@@ -239,7 +251,7 @@ def wrap_reassign(f):
     return wrapper
 
 
-@cython.test_fail_if_path_exists('//MergedDictNode')
+#@cython.test_fail_if_path_exists('//MergedDictNode')
 def kwargs_metaclass(**kwargs):
     """
     >>> K = kwargs_metaclass()