c108.json
Utilities for safe JSON file read/write/update with sensible defaults and optional atomic operations.
read_json(path, *, default=None, encoding='utf-8')
Read JSON from file with graceful error handling.
Reads and parses a JSON file, returning a default value if the file doesn't exist or contains invalid JSON. This provides a safer alternative to raw json.load() for configuration files and optional data sources.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathLike[str]
|
Path to the JSON file to read. |
required |
default
|
T
|
Value to return if file is missing or contains invalid JSON. Defaults to None. |
None
|
encoding
|
str
|
Text encoding to use when reading the file. Defaults to "utf-8". |
'utf-8'
|
Returns:
| Type | Description |
|---|---|
Any | T
|
Parsed JSON data (typically dict or list), or the default value if reading fails. |
Raises:
| Type | Description |
|---|---|
OSError
|
If file exists but cannot be read due to permissions or I/O errors (not FileNotFoundError). |
TypeError
|
If path is not a valid path-like object. |
Examples:
>>> config = read_json("config.json", default={})
>>> k = config.get("api_key", "default-key")
>>> # Type-safe with explicit default
>>> settings: dict[str, Any] = read_json(Path("settings.json"), default={})
>>> # Returns None if file missing
>>> cache = read_json("cache.json")
>>> if cache is None:
... print("No cache found")
No cache found
>>> # Custom encoding for legacy files
>>> data = read_json("legacy.json", encoding="latin-1", default=[])
Source code in c108/json.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
update_json(path, updater=None, *, key=None, value=None, default=None, encoding='utf-8', indent=2, atomic=True, ensure_ascii=False, create_parents=True)
Read JSON, apply transformation, and write back atomically.
Supports two modes of operation
- Function mode: Apply a transformation function to the entire data structure
- Key mode: Update a value at a specific key path using dot notation
The entire operation is atomic if atomic=True, preventing partial updates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathLike[str]
|
Path to the JSON file to update. |
required |
updater
|
Callable[[Any], Any] | None
|
Callable that transforms the entire data structure (current: Any) -> Any. Mutually exclusive with key parameter. |
None
|
key
|
str | None
|
Dot-separated key path for updating nested values (e.g., "database.host" or "server.settings.port"). Mutually exclusive with updater parameter. Use simple keys like "count" for top-level updates. |
None
|
value
|
Any
|
New value to set at the key path. Required when key is provided, ignored otherwise. |
None
|
default
|
Any
|
Value to use if file is missing or contains invalid JSON. Defaults to None. |
None
|
encoding
|
str
|
Text encoding for reading and writing. Defaults to "utf-8". |
'utf-8'
|
indent
|
int
|
Number of spaces for indentation in output. Use None for compact. Defaults to 2. |
2
|
atomic
|
bool
|
If True, write atomically to prevent corruption. Defaults to True. |
True
|
ensure_ascii
|
bool
|
If True, escape non-ASCII characters. Defaults to False. |
False
|
create_parents
|
bool
|
If True, automatically create missing parent dicts in nested key paths. If False, raises KeyError when intermediate keys don't exist. Only applies in key mode. Defaults to True. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both updater and key are provided, or if neither is provided, or if key is provided without value. |
TypeError
|
If updater returns non-JSON-serializable data, or if key mode is used but root data is not a dict, or if nested path encounters non-dict intermediate values. |
KeyError
|
If nested key path references missing keys and create_parents=False. |
OSError
|
If file cannot be read or written due to permissions or I/O errors. |
Exception
|
Any exception raised by the updater function is propagated to caller. |
Examples:
>>> import os, tempfile
>>> from datetime import datetime, timezone
>>> tmp = tempfile.gettempdir()
>>> file_json = os.path.join(tmp, "example_update.json")
>>> write_json(file_json, {})
>>> # Basic key mode - update top-level keys:
>>> update_json(file_json, key="last_run", value=datetime.now(timezone.utc).isoformat(), default={})
>>> update_json(file_json, key="count", value=42, default={})
>>> # Nested key mode - update deeply nested values:
>>> update_json(file_json, key="database.host", value="localhost", default={})
>>> update_json(file_json, key="server.port", value=8080, default={})
>>> update_json(file_json, key="ui.theme.colors.primary", value="#007bff", default={})
>>> # Deep nesting with automatic parent creation:
>>> update_json(file_json, key="features.experimental.beta", value=True, default={})
>>> # Result: {"features": {"experimental": {"beta": True}}}
>>> # Strict mode - fail if intermediate keys don't exist:
>>> update_json(
... file_json,
... key="database.host",
... value="localhost",
... create_parents=False,
... default={}
... ) # Raises KeyError if "database" key is missing
>>> # Function mode - complex transformations:
>>> update_json(
... file_json,
... updater=lambda cfg: {**cfg, "last_modified": datetime.now(timezone.utc).isoformat()},
... default={}
... )
>>> # Increment a counter with function mode:
>>> update_json(
... file_json,
... updater=lambda data: {"count": data.get("count", 0) + 1},
... default={}
... )
>>> # Append to a list:
>>> def add_entry(data):
... entries = data if isinstance(data, list) else []
... entries.append({"id": len(entries), "value": "new"})
... return entries
>>> update_json(file_json, updater=add_entry, default=[])
>>> # Reset file to dict for next examples
>>> write_json(file_json, {})
>>> # Update nested counter (comparing both modes):
>>> # Key mode - simpler for direct updates
>>> update_json(file_json, key="stats.visits", value=100, default={})
>>> # Function mode - needed for increments
>>> def increment_visits(data):
... if "stats" not in data:
... data["stats"] = {}
... data["stats"]["visits"] = data["stats"].get("visits", 0) + 1
... return data
>>> update_json(file_json, updater=increment_visits, default={})
>>> # Error case: Cannot use both modes
>>> update_json(file_json, lambda x: x, key="foo", value="bar")
Traceback (most recent call last):
...
ValueError: specify either updater or key, not both
>>> os.remove(file_json)
Source code in c108/json.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
write_json(path, data, *, indent=2, atomic=True, encoding='utf-8', ensure_ascii=False)
Write JSON to file with safe defaults and optional atomic write.
Writes data to a JSON file with sensible formatting defaults. Supports atomic writes to prevent data corruption if the process is interrupted mid-write (e.g., power loss, SIGKILL).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathLike[str]
|
Destination path for the JSON file. |
required |
data
|
Any
|
Python object to serialize to JSON. Must be JSON-serializable (dict, list, str, int, float, bool, None). |
required |
indent
|
int
|
Number of spaces for indentation. Use None for compact output. Defaults to 2. |
2
|
atomic
|
bool
|
If True, write to a temporary file then atomically rename to target path. This prevents corruption but may not preserve file metadata (permissions, extended attributes). Defaults to True. |
True
|
encoding
|
str
|
Text encoding to use when writing the file. Defaults to "utf-8". |
'utf-8'
|
ensure_ascii
|
bool
|
If True, escape non-ASCII characters. If False, write Unicode directly. Defaults to False. |
False
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If data is not JSON-serializable; if path is not a valid path-like object. |
OSError
|
If file cannot be written due to permissions, disk space, or I/O errors. |
ValueError
|
If indent is negative. |
Examples:
>>> import os, tempfile, json
>>> tmp = tempfile.gettempdir()
>>> path_json = os.path.join(tmp, "example.json")
>>>
>>> write_json(path_json, {"debug": True, "timeout": 30})
>>> with open(path_json, encoding="utf-8") as f:
... json.load(f)
{'debug': True, 'timeout': 30}
>>> # Compact output for space-constrained environments
>>> write_json(path_json, {"a": 1, "b": 2}, indent=None)
>>> # ASCII-safe output for legacy systems
>>> write_json(path_json, {"name": "François"}, ensure_ascii=True)
>>> os.remove(path_json)
Source code in c108/json.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |