|
1 | 1 | """ |
2 | | -Dictionary union operators allow you to merge dictionaries using |
3 | | -the | (union) and |= (in-place union) operators. These operators |
4 | | -provide a clean and intuitive syntax for combining dictionaries. |
5 | | -
|
6 | | -This feature was introduced in Python 3.9 through PEP 584. Before |
7 | | -this, you had to use methods like dict.update() or {**dict1, **dict2} |
8 | | -syntax to merge dictionaries. |
| 2 | +Dictionary union operators (`|` and `|=`) provide clean, side-effect-free dictionary |
| 3 | +merging, solving a fundamental software engineering challenge in state management. |
| 4 | +
|
| 5 | +In production engineering, managing application state and configuration pipelines |
| 6 | +often requires combining multiple data sources (such as default settings, environment |
| 7 | +variables, and user overrides). Prior to Python 3.9 (PEP 584), merging required |
| 8 | +destructive in-place mutations via `dict.update()` or verbose `{**d1, **d2}` unpacking, |
| 9 | +which often introduced subtle shared-state bugs across layers. |
| 10 | +
|
| 11 | +Key software engineering benefits: |
| 12 | +- Immutability & Safety: `|` produces a new dictionary without mutating either operand, |
| 13 | + preventing side-effects across components and concurrent execution paths. |
| 14 | +- Deterministic Precedence: Subsequent dictionaries cleanly override earlier ones, |
| 15 | + making layered configuration cascades explicit and predictable. |
| 16 | +- Expressive In-Place Updates: `|=` clearly signals intentional local mutation |
| 17 | + without sacrificing readability. |
9 | 18 | """ |
10 | 19 |
|
| 20 | +from typing import Any |
| 21 | + |
| 22 | + |
| 23 | +def cascade_config(base: dict[str, Any], *overrides: dict[str, Any]) -> dict[str, Any]: |
| 24 | + """Cascade configuration layers without mutating input dictionaries. |
| 25 | +
|
| 26 | + Later layers override earlier ones, returning a freshly merged dictionary. |
| 27 | + """ |
| 28 | + merged = base.copy() |
| 29 | + for override in overrides: |
| 30 | + merged |= override |
| 31 | + return merged |
| 32 | + |
| 33 | + |
| 34 | +def apply_patch(record: dict[str, Any], patch: dict[str, Any]) -> None: |
| 35 | + """Apply a partial update in-place to an existing record using |=.""" |
| 36 | + record |= patch |
| 37 | + |
11 | 38 |
|
12 | 39 | def main() -> None: |
13 | | - # Traditional dictionary merging before Python 3.9 |
14 | | - # Method 1: Using dict.update() (modifies the original) |
15 | | - dict1_old = {"a": 1, "b": 2} |
16 | | - dict2_old = {"c": 3, "d": 4} |
17 | | - dict1_old.update(dict2_old) |
18 | | - assert dict1_old == {"a": 1, "b": 2, "c": 3, "d": 4} |
19 | | - |
20 | | - # Method 2: Using dictionary unpacking (creates a new dict) |
21 | | - dict3_old = {"a": 1, "b": 2} |
22 | | - dict4_old = {"c": 3, "d": 4} |
23 | | - merged_old = {**dict3_old, **dict4_old} |
24 | | - assert merged_old == {"a": 1, "b": 2, "c": 3, "d": 4} |
25 | | - |
26 | | - # With Python 3.9+, we can use the | operator for union |
27 | | - # This creates a new dictionary without modifying the originals |
| 40 | + # 1. Non-mutating dictionary union (|) |
28 | 41 | dict1 = {"a": 1, "b": 2} |
29 | 42 | dict2 = {"c": 3, "d": 4} |
30 | 43 | merged = dict1 | dict2 |
31 | 44 | assert merged == {"a": 1, "b": 2, "c": 3, "d": 4} |
32 | | - |
33 | | - # The original dictionaries remain unchanged |
| 45 | + # Original dictionaries remain untouched |
34 | 46 | assert dict1 == {"a": 1, "b": 2} |
35 | 47 | assert dict2 == {"c": 3, "d": 4} |
36 | 48 |
|
37 | | - # When there are overlapping keys, the right operand's values take precedence |
38 | | - # This is the same behavior as dict.update() and {**d1, **d2} |
| 49 | + # 2. Overlapping key precedence: right operand overrides left operand |
39 | 50 | dict3 = {"a": 1, "b": 2, "c": 3} |
40 | 51 | dict4 = {"b": 20, "c": 30, "d": 4} |
41 | 52 | merged2 = dict3 | dict4 |
42 | | - # Keys 'b' and 'c' from dict4 override those from dict3 |
43 | 53 | assert merged2 == {"a": 1, "b": 20, "c": 30, "d": 4} |
44 | 54 |
|
45 | | - # The order matters! Left operand is the base, right operand overwrites |
| 55 | + # Order matters: left is the base, right is the override |
46 | 56 | merged3 = dict4 | dict3 |
47 | | - # Now keys 'b' and 'c' from dict3 override those from dict4 |
48 | 57 | assert merged3 == {"b": 2, "c": 3, "d": 4, "a": 1} |
49 | 58 |
|
50 | | - # The |= operator performs in-place union (augmented assignment) |
51 | | - # This is equivalent to dict.update() but with cleaner syntax |
| 59 | + # 3. In-place dictionary union (|=) |
| 60 | + # Mutates the left operand in place, leaving the right operand unchanged |
52 | 61 | dict5 = {"a": 1, "b": 2} |
53 | | - dict6 = {"c": 3, "d": 4} |
| 62 | + dict6 = {"b": 20, "c": 3} |
54 | 63 | dict5 |= dict6 |
55 | | - # dict5 is modified in place |
56 | | - assert dict5 == {"a": 1, "b": 2, "c": 3, "d": 4} |
57 | | - # dict6 remains unchanged |
58 | | - assert dict6 == {"c": 3, "d": 4} |
59 | | - |
60 | | - # The |= operator also handles overlapping keys |
61 | | - dict7 = {"a": 1, "b": 2, "c": 3} |
62 | | - dict8 = {"b": 20, "d": 4} |
63 | | - dict7 |= dict8 |
64 | | - # 'b' from dict8 overwrites 'b' in dict7 |
65 | | - assert dict7 == {"a": 1, "b": 20, "c": 3, "d": 4} |
66 | | - |
67 | | - # You can chain multiple | operations |
68 | | - dict9 = {"a": 1} |
69 | | - dict10 = {"b": 2} |
70 | | - dict11 = {"c": 3} |
71 | | - dict12 = {"d": 4} |
72 | | - combined = dict9 | dict10 | dict11 | dict12 |
73 | | - assert combined == {"a": 1, "b": 2, "c": 3, "d": 4} |
74 | | - |
75 | | - # When chaining with overlapping keys, rightmost values win |
76 | | - dict13 = {"a": 1, "x": 10} |
77 | | - dict14 = {"b": 2, "x": 20} |
78 | | - dict15 = {"c": 3, "x": 30} |
79 | | - combined2 = dict13 | dict14 | dict15 |
80 | | - # 'x' ends up with value 30 from the rightmost dictionary |
81 | | - assert combined2 == {"a": 1, "b": 2, "c": 3, "x": 30} |
82 | | - |
83 | | - # The union operator works with empty dictionaries |
84 | | - empty: dict[str, int] = {} |
85 | | - dict16 = {"a": 1, "b": 2} |
86 | | - assert empty | dict16 == {"a": 1, "b": 2} |
87 | | - assert dict16 | empty == {"a": 1, "b": 2} |
88 | | - assert empty | empty == {} |
89 | | - |
90 | | - # The union operator can be used with dict() constructor results |
91 | | - dict17 = dict(a=1, b=2) |
92 | | - dict18 = dict(c=3, d=4) |
93 | | - merged4 = dict17 | dict18 |
94 | | - assert merged4 == {"a": 1, "b": 2, "c": 3, "d": 4} |
95 | | - |
96 | | - # You can mix different value types in merged dictionaries |
97 | | - dict19 = {"name": "Alice", "age": 30} |
98 | | - dict20 = {"city": "NYC", "scores": [85, 90, 95]} |
99 | | - dict21 = {"active": True} |
100 | | - person = dict19 | dict20 | dict21 |
101 | | - assert person == {"name": "Alice", "age": 30, "city": "NYC", "scores": [85, 90, 95], "active": True} |
102 | | - |
103 | | - # Practical use case: Configuration merging |
104 | | - # Start with default configuration |
105 | | - default_config = {"timeout": 30, "retries": 3, "debug": False, "log_level": "INFO"} |
| 64 | + assert dict5 == {"a": 1, "b": 20, "c": 3} |
| 65 | + assert dict6 == {"b": 20, "c": 3} |
106 | 66 |
|
107 | | - # User provides custom configuration (partial) |
108 | | - user_config = {"timeout": 60, "debug": True} |
| 67 | + # 4. Chained unions |
| 68 | + dict_a = {"x": 1} |
| 69 | + dict_b = {"y": 2} |
| 70 | + dict_c = {"z": 3, "x": 10} |
| 71 | + combined = dict_a | dict_b | dict_c |
| 72 | + assert combined == {"x": 10, "y": 2, "z": 3} |
109 | 73 |
|
110 | | - # Merge configurations, user settings override defaults |
111 | | - final_config = default_config | user_config |
112 | | - assert final_config == { |
113 | | - "timeout": 60, # Overridden by user |
114 | | - "retries": 3, # From default |
115 | | - "debug": True, # Overridden by user |
116 | | - "log_level": "INFO", # From default |
117 | | - } |
118 | | - |
119 | | - # Practical use case: Building objects incrementally |
120 | | - # Start with base attributes |
121 | | - base = {"id": 1, "type": "user"} |
122 | | - |
123 | | - # Add authentication info |
124 | | - with_auth = base | {"username": "john", "email": "john@example.com"} |
125 | | - |
126 | | - # Add profile info |
127 | | - with_profile = with_auth | {"bio": "Developer", "location": "USA"} |
| 74 | + # 5. Empty dictionaries and dict() constructors |
| 75 | + empty: dict[str, int] = {} |
| 76 | + assert empty | {"a": 1} == {"a": 1} |
| 77 | + assert dict(a=1, b=2) | dict(c=3) == {"a": 1, "b": 2, "c": 3} |
128 | 78 |
|
129 | | - assert with_profile == {"id": 1, "type": "user", "username": "john", "email": "john@example.com", "bio": "Developer", "location": "USA"} |
| 79 | + # 6. Practical engineering: Cascading configuration layers |
| 80 | + default_config = {"timeout": 30, "retries": 3, "debug": False, "log_level": "INFO"} |
| 81 | + env_config = {"debug": True} |
| 82 | + cli_config = {"timeout": 60, "log_level": "DEBUG"} |
| 83 | + |
| 84 | + active_config = cascade_config(default_config, env_config, cli_config) |
| 85 | + assert active_config == { |
| 86 | + "timeout": 60, # Overridden by CLI |
| 87 | + "retries": 3, # Retained from defaults |
| 88 | + "debug": True, # Overridden by environment |
| 89 | + "log_level": "DEBUG", # Overridden by CLI |
| 90 | + } |
| 91 | + # Verified: default_config was never mutated |
| 92 | + assert default_config["timeout"] == 30 |
| 93 | + assert default_config["debug"] is False |
130 | 94 |
|
131 | | - # Practical use case: Updating records with |= |
| 95 | + # 7. Practical engineering: Applying in-place record patches |
132 | 96 | user_record = {"id": 100, "name": "Jane", "status": "active", "login_count": 5} |
| 97 | + patch_data = {"status": "inactive", "login_count": 6} |
| 98 | + apply_patch(user_record, patch_data) |
| 99 | + assert user_record == {"id": 100, "name": "Jane", "status": "inactive", "login_count": 6} |
133 | 100 |
|
134 | | - # Apply update from an external source |
135 | | - update = {"status": "inactive", "login_count": 6, "last_login": "2024-01-15"} |
136 | | - user_record |= update |
137 | | - |
138 | | - assert user_record == {"id": 100, "name": "Jane", "status": "inactive", "login_count": 6, "last_login": "2024-01-15"} |
139 | | - |
140 | | - # The union operators only work with dictionaries |
141 | | - # Attempting to use them with non-dict types raises TypeError |
142 | | - dict22 = {"a": 1} |
| 101 | + # 8. Type safety: Unions require both operands to be dict instances |
143 | 102 | error_raised = False |
144 | 103 | try: |
145 | | - # This will fail because list is not a dict |
146 | | - dict22 | [("b", 2)] # type: ignore [operator] |
| 104 | + dict1 | [("c", 3)] # type: ignore [operator] |
147 | 105 | except TypeError: |
148 | 106 | error_raised = True |
149 | 107 | assert error_raised is True |
150 | 108 |
|
151 | | - # However, you can use dict() to convert compatible types first |
152 | | - dict23 = {"a": 1} |
153 | | - dict24 = dict([("b", 2), ("c", 3)]) # Convert list of tuples to dict |
154 | | - merged5 = dict23 | dict24 |
155 | | - assert merged5 == {"a": 1, "b": 2, "c": 3} |
156 | | - |
157 | | - # Comparison with the older approaches shows the clarity improvement: |
158 | | - |
159 | | - # OLD: Using update() - modifies original, no expression result |
160 | | - old1 = {"a": 1} |
161 | | - old1.update({"b": 2}) |
162 | | - assert old1 == {"a": 1, "b": 2} |
| 109 | + # Non-dict compatible mappings must be converted explicitly |
| 110 | + assert dict1 | dict([("c", 3)]) == {"a": 1, "b": 2, "c": 3} |
163 | 111 |
|
164 | | - # OLD: Using unpacking - verbose for multiple merges |
165 | | - old2 = {**{"a": 1}, **{"b": 2}, **{"c": 3}} |
166 | | - assert old2 == {"a": 1, "b": 2, "c": 3} |
| 112 | + # 9. Comparison with pre-3.9 approaches |
| 113 | + # Traditional update() mutated in place and returned None (no chaining): |
| 114 | + legacy_dict = {"a": 1} |
| 115 | + legacy_dict.update({"b": 2}) |
| 116 | + assert legacy_dict == {"a": 1, "b": 2} |
167 | 117 |
|
168 | | - # NEW: Using union operator - clean and chainable |
169 | | - new1 = {"a": 1} | {"b": 2} | {"c": 3} |
170 | | - assert new1 == {"a": 1, "b": 2, "c": 3} |
| 118 | + # Traditional unpacking syntax was verbose: |
| 119 | + legacy_unpacking = {**{"a": 1}, **{"b": 2}} |
| 120 | + assert legacy_unpacking == {"a": 1, "b": 2} |
171 | 121 |
|
172 | 122 |
|
173 | 123 | if __name__ == "__main__": |
|
0 commit comments