How to pretty print JSON in Python
Learn how to pretty print JSON in Python. We cover different methods, tips, real-world applications, and common errors for easy debugging.

Pretty printing JSON in Python transforms raw data into a human readable format. This is essential for debugging and analysis. Python’s built in json module makes this task straightforward.
You'll explore several techniques to format JSON output, with practical tips for real world applications. You'll also get debugging advice to help you handle JSON data more effectively.
Using json.dumps() with indentation
import json
data = {"name": "John", "age": 30, "city": "New York"}
pretty_json = json.dumps(data, indent=4)
print(pretty_json)--OUTPUT--{
"name": "John",
"age": 30,
"city": "New York"
}
The json.dumps() function serializes a Python dictionary into a JSON formatted string. The key to pretty printing is the indent parameter. When you set indent=4, you’re telling Python to format the string with an indentation of four spaces for each level of the JSON structure, making it readable.
This simple argument transforms a compact, single-line string into a structured output that's much easier to parse. This is invaluable during debugging, as it lets you quickly inspect the structure and contents of your JSON data without straining to follow nested brackets. This approach is also memory-efficient when working with large datasets.
Standard pretty printing techniques
While json.dumps() is a powerful starting point, Python offers other methods for handling complex data structures, files, and custom formatting requirements.
Using the pprint module for JSON structures
import json
from pprint import pprint
data = {"name": "John", "age": 30, "city": "New York", "skills": ["Python", "JavaScript"]}
pprint(data)--OUTPUT--{'age': 30,
'city': 'New York',
'name': 'John',
'skills': ['Python', 'JavaScript']}
Python's pprint module is specifically designed to print complex data structures in a more readable way. Unlike json.dumps(), which serializes an object into a JSON formatted string, pprint() formats the Python object itself for display.
- It automatically sorts dictionary keys alphabetically, which can make comparing different objects easier.
- It intelligently handles indentation and line breaks for nested data, improving clarity without extra configuration.
Notice the output uses single quotes; this is a clear sign you're viewing a Python dictionary's string representation, not a formal JSON string.
Pretty printing JSON from a file
import json
with open('data.json', 'r') as file:
data = json.load(file)
formatted_json = json.dumps(data, indent=2)
print(formatted_json)--OUTPUT--{
"name": "John",
"age": 30,
"city": "New York",
"skills": [
"Python",
"JavaScript"
]
}
When your JSON data is in a file, you'll use the json.load() function, following similar principles to reading text files in Python. It reads the file's content and parses it directly into a Python dictionary—a key difference from json.loads(), which operates on strings.
- The process starts by opening the file in read mode.
- Next,
json.load()deserializes the JSON data into a Python object. - Finally, you can pass this object to
json.dumps()to get a nicely formatted string for printing or saving.
Customizing JSON output with sort_keys and separators
import json
data = {"city": "New York", "name": "John", "age": 30}
pretty_json = json.dumps(data, indent=4, sort_keys=True, separators=(',', ': '))
print(pretty_json)--OUTPUT--{
"age": 30,
"city": "New York",
"name": "John"
}
You can gain more control over your JSON output by using additional arguments in json.dumps(). These parameters let you fine-tune the formatting for consistency and readability.
- Setting
sort_keys=Trueorganizes the dictionary keys alphabetically, similar to techniques for sorting dictionaries in Python. This creates a predictable order, which is helpful when you need to compare different JSON objects. - The
separatorsargument customizes the delimiters. It's a tuple where the first element separates items and the second separates keys from values. Using(',', ': ')is standard for pretty-printing, ensuring a space follows the colon for clarity.
Advanced JSON formatting approaches
For more complex needs, you can move beyond standard formatting with specialized tools for colorizing output, handling custom objects, and querying data structures.
Colorizing JSON output with the pygments library
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
data = {"name": "John", "age": 30, "city": "New York"}
json_str = json.dumps(data, indent=2)
colored_json = highlight(json_str, JsonLexer(), TerminalFormatter())
print(colored_json)--OUTPUT--{
"name": "John",
"age": 30,
"city": "New York"
}
For even greater readability, you can use the pygments library to add syntax highlighting to your JSON output. This makes keys, values, and punctuation visually distinct, which is a huge help when scanning data in a terminal. The core of this technique is the highlight() function.
- It uses a
JsonLexerto understand the structure of your JSON string. - A
TerminalFormatterthen applies colors suitable for a command-line interface.
You simply pass your standard pretty-printed string to the function to get the colorized version.
Creating a custom JSON encoder for complex objects
import json
from datetime import datetime
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
data = {"name": "John", "timestamp": datetime.now()}
print(json.dumps(data, indent=2, cls=CustomEncoder))--OUTPUT--{
"name": "John",
"timestamp": "2023-07-19T15:30:45.123456"
}
The json module can't serialize complex Python objects like datetime by default. To work around this, you can create a custom encoder by subclassing json.JSONEncoder and overriding its default() method. This method lets you define how to convert non-standard objects into something JSON understands, including techniques for converting datetime objects to strings.
- You check the object's type—for example, if it's a
datetimeinstance. - You then return a serializable version, like a string from the
isoformat()method.
Finally, pass your custom class to json.dumps() using the cls argument to apply the new logic.
Using jmespath for JSON filtering and formatting
import json
import jmespath
data = {"users": [{"name": "John", "age": 30}, {"name": "Alice", "age": 25}]}
query = jmespath.compile('users[?age > `28`]')
result = query.search(data)
print(json.dumps(result, indent=2))--OUTPUT--[
{
"name": "John",
"age": 30
}
]
When you only need a subset of your JSON data, the jmespath library is a powerful tool. It acts as a query language for JSON, letting you declaratively extract the information you want without writing complex loops or conditional logic.
- The expression
'users[?age > `28`]'filters the list, returning only users older than 28. - You first prepare this query with
jmespath.compile()for better performance, then execute it using thesearch()method on your data.
This approach simplifies data extraction, especially from deeply nested structures, giving you just the results you need.
Move faster with Replit
Replit is an AI-powered development platform that comes with all Python dependencies pre-installed, so you can skip setup and start coding instantly. With Agent 4, you can move from piecing together techniques to building complete apps, as it handles the code, databases, APIs, and deployment directly from a description.
Instead of just formatting JSON, you can build practical tools that use it:
- An API response viewer that fetches data, filters it for specific fields, and displays it as color-coded, pretty-printed
JSONfor quick debugging. - A log file converter that processes raw text logs, serializes them into a structured
JSONformat with custom timestamps, and sorts the keys alphabetically for consistent analysis. - A dynamic configuration generator that takes simple user inputs and creates a perfectly indented
JSONfile for use in other applications or services.
Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.
Common errors and challenges
Even with Python's helpful tools, you might run into issues like non-serializable objects, encoding errors, or circular references when formatting JSON.
Handling non-serializable objects with json.dumps()
You'll sometimes see a TypeError stating an object is not JSON serializable. This happens because json.dumps() only understands Python's built-in types like strings, numbers, lists, and dictionaries. When it encounters a complex type like a datetime object or a custom class instance, it doesn't know how to convert it.
The solution is to teach the function how to handle these objects. By providing a custom encoder, you can specify a conversion rule—for example, turning a datetime object into an ISO 8601 string—before serialization occurs. This type of error handling and code repair is essential for robust JSON processing.
Fixing encoding issues with non-ASCII characters
By default, json.dumps() escapes any non-ASCII characters, converting them into \uXXXX sequences. While this ensures maximum compatibility, it makes the output harder to read if your data contains special characters or different languages.
To fix this, you can set the ensure_ascii=False parameter in your json.dumps() call. This tells Python to write the characters directly into the string, preserving their original form and improving readability.
Resolving circular references in object serialization
A circular reference occurs when an object refers to another object that, in turn, refers back to the first one, creating an infinite loop. When json.dumps() tries to serialize such a structure, it gets trapped and raises a ValueError.
There's no simple parameter to fix this. The best approach is to restructure your data before serialization to eliminate the circular dependency. This often means creating a simplified dictionary representation of your objects that contains only the necessary data without the problematic cross-references.
Handling non-serializable objects with json.dumps()
A common roadblock is the TypeError you get when json.dumps() encounters data it can't process, like a datetime object. The code below triggers this error by attempting to serialize a dictionary containing an object that isn't natively supported.
import json
from datetime import datetime
data = {"name": "John", "created_at": datetime.now()}
json_string = json.dumps(data, indent=2)
print(json_string)
The json.dumps() function fails because the datetime.now() object isn't a standard data type it can serialize, which causes a TypeError. The following code demonstrates how to resolve this by providing a custom conversion rule.
import json
from datetime import datetime
data = {"name": "John", "created_at": datetime.now()}
json_string = json.dumps(data, indent=2, default=str)
print(json_string)
The solution is to pass default=str to the json.dumps() function. This acts as a fallback, telling the serializer to convert any unsupported object into its string form. It’s a practical shortcut for handling types like datetime without needing a custom encoder, especially when your data includes timestamps or other complex objects from sources like databases or APIs.
Fixing encoding issues with non-ASCII characters
By default, json.dumps() prioritizes compatibility by escaping non-ASCII characters into \uXXXX sequences, which can make your output difficult to read. This is common when data includes different languages or emojis. The following code demonstrates this default behavior in action.
import json
data = {"message": "Hello, 世界!", "emoji": "😊"}
json_string = json.dumps(data)
print(json_string)
The output shows how json.dumps() replaces the Chinese characters and emoji with \uXXXX escape codes, making the string hard to read. The following code shows how to preserve the original characters for better clarity.
import json
data = {"message": "Hello, 世界!", "emoji": "😊"}
json_string = json.dumps(data, ensure_ascii=False, indent=2)
print(json_string)
Setting ensure_ascii=False in json.dumps() solves the problem by telling the function to write non-ASCII characters directly. This makes the output readable, which is crucial when working with data containing different languages or symbols like emojis. It’s a simple change that significantly improves clarity when you're inspecting or debugging JSON that isn't purely English text. This is particularly important when your application handles international user data or content from global APIs.
Resolving circular references in object serialization
A circular reference occurs when an object refers back to itself, either directly or through another object, creating an endless loop. When json.dumps() encounters this, it can't complete the serialization and raises a ValueError. The following code triggers this error.
import json
class Person:
def __init__(self, name):
self.name = name
self.friends = []
john = Person("John")
john.friends.append(john) # Self-reference
json_string = json.dumps(john.__dict__, indent=2)
Here, the john object is added to its own friends list. This self-reference causes json.dumps() to enter an infinite loop and fail. See how to restructure the data to prevent this in the following example.
import json
class Person:
def __init__(self, name):
self.name = name
self.friends = []
def serialize_person(person):
return {"name": person.name, "friends_count": len(person.friends)}
john = Person("John")
john.friends.append(john)
json_string = json.dumps(serialize_person(john), indent=2)
The solution is to manually create a "safe" dictionary for serialization instead of passing the object directly. The serialize_person() function builds a new dictionary with only the necessary data, replacing the problematic self-referencing list with a simple count. This breaks the infinite loop. You'll often encounter this issue when working with complex data structures from databases or APIs where objects are linked together, so it's a good pattern to remember.
Real-world applications
Moving beyond troubleshooting, you’ll find that pretty printing is essential for building structured API responses and detecting changes in configuration files.
Building structured API responses with json.dumps()
When building an API, using json.dumps() with indentation ensures your responses are consistently structured and easy for developers to read.
import json
def create_api_response(success, data=None, error=None):
response = {"success": success, "timestamp": "2023-07-20T10:15:30Z"}
if data: response["data"] = data
if error: response["error"] = error
return json.dumps(response, indent=2)
print(create_api_response(True, data={"username": "john_doe", "points": 100}))
The create_api_response function is a factory for generating standardized JSON, which can be extended for creating JSON files. It builds a response dictionary that always includes a success status and a timestamp, ensuring a consistent format for every API call.
- It flexibly adds a
datapayload or anerrormessage depending on the outcome. - The final step uses
json.dumps()withindent=2to convert the Python dictionary into a properly formatted JSON string, ready to be sent as an API response.
Detecting configuration changes with json.dumps()
Another great use for json.dumps() is comparing configuration files, since it can normalize them into a consistent, sorted format that makes spotting differences easy. This type of automated configuration management is perfect for vibe coding workflows.
import json
import difflib
def compare_configs(old_config, new_config):
old_json = json.dumps(old_config, indent=2, sort_keys=True).splitlines()
new_json = json.dumps(new_config, indent=2, sort_keys=True).splitlines()
return list(difflib.unified_diff(old_json, new_json, lineterm=''))
old = {"server": "prod1", "port": 8000, "debug": True}
new = {"server": "prod1", "port": 8080, "debug": False}
for line in compare_configs(old, new):
print(line)
The compare_configs function serializes both dictionaries into standardized JSON strings using json.dumps() with sort_keys=True. This step is crucial for an accurate comparison, as it ensures the order of keys doesn't matter. The strings are then split into individual lines.
- The function feeds these lines into Python’s
difflib.unified_diff(). - This generates a "unified diff" output, clearly marking which lines were changed between the old and new configurations, making it easy to spot modifications.
Get started with Replit
Now, turn these techniques into a real tool. Describe what you want to build to Replit Agent, like “a tool that pretty prints API responses” or “a script that diffs two JSON config files.”
Replit Agent writes the code, tests for errors, and deploys your app from a simple description. Start building with Replit.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.
Describe what you want to build, and Replit Agent writes the code, handles the infrastructure, and ships it live. Go from idea to real product, all in your browser.



