Write JSON to File in Python

Python's built-in json module makes it straightforward to write JSON to a file. The two key functions are json.dump(), which writes directly to a file object, and json.dumps(), which returns the JSON as a string. This guide covers both functions with practical examples: pretty printing, writing arrays, appending records with NDJSON, handling non-serializable types, and performing atomic writes to prevent data corruption.

Validate your JSON structure in Jsonic before writing it to disk.

Open JSON Formatter

json.dump vs json.dumps

The json module exposes two write functions with similar names but different outputs. json.dump serialises an object and writes it directly to an open file; json.dumps does the same but returns a Python string instead. Choose json.dump for file I/O and json.dumps when you need the JSON text in memory — for example, to pass it to an HTTP response or store it in a database column.

import json

data = {"name": "Alice", "age": 30, "active": True}

# json.dump → writes to file object
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f)

# json.dumps → returns string
json_str = json.dumps(data)
print(json_str)  # {"name": "Alice", "age": 30, "active": true}

Note that Python's True is serialised as JSON true (lower case), and None becomes null. Both functions accept the same keyword arguments: indent, sort_keys, ensure_ascii,separators, and cls.

Basic write — open() + json.dump

The minimal pattern is to open a file in write mode ("w") with UTF-8 encoding and pass the file object as the second argument to json.dump. The context manager closes the file automatically when the block exits.

import json

data = {
    "user": "alice",
    "scores": [95, 87, 92],
    "meta": {"country": "US", "verified": True}
}

with open("output.json", "w", encoding="utf-8") as f:
    json.dump(data, f)
# Writes: {"user": "alice", "scores": [95, 87, 92], "meta": {"country": "US", "verified": true}}

Always specify encoding="utf-8" explicitly. On Windows the default encoding can be cp1252, which silently corrupts non-ASCII characters such as accented letters or emoji.

Pretty print with indent and sort_keys

By default json.dump writes compact JSON with no whitespace. Passindent=2 (or indent=4) to produce human-readable output with newlines and indentation. Add sort_keys=True to output keys in alphabetical order, which is useful for deterministic diffs in version control.

import json

data = {"z_key": "last", "a_key": "first", "nested": {"b": 2, "a": 1}}

with open("pretty.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, sort_keys=True)

# Output:
# {
#   "a_key": "first",
#   "nested": {
#     "a": 1,
#     "b": 2
#   },
#   "z_key": "last"
# }

# With 4-space indent (common in JavaScript projects)
with open("pretty4.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=4)

Use indent=2 for most web APIs and configs. Use indent=4when interoperating with JavaScript tooling that defaults to 4-space indentation. For the smallest possible file size, omit indent and use separators=(',', ':') to strip all whitespace including the space after colons.

Write a list of objects (JSON array)

When your data is a list of dicts, pass the list directly to json.dump. The output is a JSON array. You can read it back with json.load and iterate over the resulting list.

import json

users = [
    {"id": 1, "name": "Alice", "role": "admin"},
    {"id": 2, "name": "Bob",   "role": "viewer"},
    {"id": 3, "name": "Carol", "role": "editor"},
]

with open("users.json", "w", encoding="utf-8") as f:
    json.dump(users, f, indent=2)

# Reads back
with open("users.json", encoding="utf-8") as f:
    loaded = json.load(f)
print(len(loaded))  # 3

For large arrays (millions of records) this approach loads everything into memory at once. See the NDJSON section below for a streaming alternative that writes one record per line without loading the whole dataset.

Append records with NDJSON (JSONL)

Standard JSON cannot be appended to because a valid JSON file has exactly one root value. Opening an existing file in append mode ("a") would produce invalid JSON. The solution is Newline Delimited JSON (NDJSON or JSONL): one JSON object per line, with no surrounding array brackets. Each line is valid JSON on its own, so you can append with a normal file append and read back line by line.

import json

# Writing NDJSON (one JSON object per line)
records = [
    {"event": "login",  "user": "alice", "ts": "2024-01-15T10:00:00Z"},
    {"event": "logout", "user": "alice", "ts": "2024-01-15T10:45:00Z"},
]

with open("events.ndjson", "w", encoding="utf-8") as f:
    for record in records:
        f.write(json.dumps(record) + "\n")

# Append more records later (no need to read the whole file)
new_record = {"event": "login", "user": "bob", "ts": "2024-01-15T11:00:00Z"}
with open("events.ndjson", "a", encoding="utf-8") as f:
    f.write(json.dumps(new_record) + "\n")

# Read NDJSON back
with open("events.ndjson", encoding="utf-8") as f:
    loaded = [json.loads(line) for line in f]

NDJSON is widely used for log files, event streams, and data pipelines. Tools likejq, BigQuery, and Spark natively support the NDJSON format, making it easy to process large files without loading them fully into memory.

Update an existing JSON file

To modify a JSON file — for example, updating a config — read the current contents with json.load, update the Python object, then write it back withjson.dump.

import json

# Read existing data
with open("config.json", encoding="utf-8") as f:
    config = json.load(f)

# Modify
config["version"] = "2.0"
config["features"]["dark_mode"] = True

# Write back
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2)

This pattern reads the full file into memory, modifies it, and overwrites the file. For production use where a crash between the read and write could leave the file partially written, see the atomic write pattern in the next section.

Custom JSONEncoder for non-serializable types

By default json.dump raises TypeError for types it does not know how to serialise — for example datetime, Decimal, or custom classes. Subclass json.JSONEncoder and override the default method to handle each type, then pass your encoder class with thecls keyword argument.

import json
from datetime import datetime, date
from decimal import Decimal

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return str(obj)
        return super().default(obj)

data = {
    "created_at": datetime(2024, 1, 15, 10, 30, 0),
    "price": Decimal("19.99"),
    "name": "Widget",
}

with open("output.json", "w", encoding="utf-8") as f:
    json.dump(data, f, cls=CustomEncoder, indent=2)

# Output:
# {
#   "created_at": "2024-01-15T10:30:00",
#   "price": "19.99",
#   "name": "Widget"
# }

Always call super().default(obj) at the end of the method. This propagates the original TypeError for truly unserializable types rather than silently producing incorrect output. For numpy arrays, return obj.tolist() to convert to a plain Python list.

Atomic write (safe write with os.replace)

A plain open("file.json", "w") truncates the file before writing. If the process crashes mid-write — due to power loss, SIGKILL, or an unhandled exception — the file is left empty or partially written. The atomic write pattern avoids this: write to a temporary file in the same directory, then rename it over the destination. os.replace is atomic on POSIX systems, meaning the target is either fully replaced or completely unchanged.

import json
import os
import tempfile

def write_json_atomic(path: str, data: object, indent: int = 2) -> None:
    """Write JSON atomically: temp file then rename — safe against crashes."""
    dir_name = os.path.dirname(os.path.abspath(path))
    tmp_fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp")
    try:
        with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=indent)
        os.replace(tmp_path, path)  # atomic on POSIX
    except Exception:
        os.unlink(tmp_path)         # clean up on failure
        raise

# Usage
write_json_atomic("config.json", {"version": "1.0", "debug": False})

The temporary file is created in the same directory as the target so that os.replace performs a rename within the same filesystem. Cross-filesystem renames are not atomic. This pattern is strongly recommended for any configuration file or persistent state that your application depends on.

json.dump parameter reference

ParameterDefaultEffect
indentNone (compact)Number of spaces for indentation (2 or 4 common)
sort_keysFalseSort dictionary keys alphabetically
ensure_asciiTrueEscape non-ASCII chars; set False to keep Unicode
separators(', ', ': ')Use (',', ':') to minimize whitespace
clsNoneCustom JSONEncoder subclass

Frequently asked questions

What is the difference between json.dump and json.dumps?

json.dump(obj, file) writes JSON directly to an open file object. json.dumps(obj) returns the JSON as a Python string. Use json.dump when writing to a file and json.dumps when you need the string in memory — for example, to send as an HTTP response body or store in a database column.

How do I append a record to an existing JSON file?

JSON files are not append-friendly because valid JSON has one root value. The standard approach is to read the file into memory (json.load), modify the Python object, then write it back with json.dump. For high-frequency appends, use NDJSON/JSONL instead: one JSON object per line, which lets you append with open('file.jsonl', 'a') and write each record as json.dumps(record) + "\n".

How do I write JSON with indentation in Python?

Pass the indent parameter to json.dump or json.dumps: json.dump(data, f, indent=2) uses 2-space indentation, indent=4 uses 4-space tabs. You can also add sort_keys=True to sort dictionary keys alphabetically, which is useful for deterministic output and diffing.

How do I write non-serializable objects to JSON in Python?

Create a subclass of json.JSONEncoder and override the default(self, obj) method to return a serializable form for your custom type. Pass it as the cls parameter: json.dump(data, f, cls=MyEncoder). Common cases are datetime (convert to ISO string), Decimal (convert to float or str), and numpy arrays (call .tolist()).

How do I write a list of objects to a JSON file?

Pass the list directly: json.dump(records, f, indent=2) where records is a Python list of dicts. The output is a JSON array. Alternatively, use NDJSON for large lists: write each dict as a separate line with json.dumps(record) so you can stream records without loading the full array into memory.

How do I write JSON to a file safely to avoid data loss?

Use an atomic write: write to a temporary file in the same directory, then use os.replace(tmp_path, target_path) to rename it. os.replace is atomic on POSIX systems — the destination file is either fully written or unchanged, so a crash mid-write does not leave a corrupted file.

Validate your JSON

Paste your JSON into Jsonic to check for syntax errors and inspect the structure before writing it to disk with your Python script.

Open JSON Formatter