Python Dict to JSON Converter

Convert Python dictionaries to valid JSON format

Python Dictionary
JSON Output
Copied to clipboard!

Python Dict to JSON Converter: What It Is and How It Works

A Python dict to JSON converter turns a Python dictionary into JSON text. The standard library handles this through the built-in json module, so no install is needed.

The dictionary lives in memory as a native Python object. JSON is a string, meant to travel between systems or sit in a file.

That distinction matters more than it looks.

A dict can hold live Python objects: sets, custom classes, functions even. JSON only understands a handful of primitive types.

  • Objects, mapped from Python dicts
  • Arrays, mapped from Python lists and tuples
  • Strings, numbers, booleans, and null

Anything outside that list has to be translated first, which is where most of the friction in this process actually shows up.

JSON is standardized as RFC 8259, published by the Internet Engineering Task Force in December 2017.

That standard is what makes a JSON file produced by Python readable by JavaScript, Java, Go, or pretty much anything else without a translation layer in between.

If you want a quick reference for dict syntax while working through conversions, the python cheat sheet covers the basics without the fluff.

Format converters aren't limited to dicts either. Teams pulling data from spreadsheets often reach for a dedicated CSV to JSON converter instead, since the source there is rows and columns rather than a live object. When the source is a config file, a YAML to JSON converter covers the same ground for YAML input.

json.dumps() vs json.dump(): Choosing the Right Method

Both functions do the same core job. They walk through a Python dict and convert it to JSON.

The difference is what they hand back.

Featurejson.dumps()json.dump()
Return valueJSON stringNone, writes to a file
Typical useAPIs, logging, in-memory workSaving data to disk
Required argumentThe object to serializeThe object, plus a file object

json.dumps() syntax and return value

Returns a string:

  • Takes the dict as its first argument
  • Accepts keyword arguments like indent, sort_keys, and default
  • Hands back a str, nothing gets written anywhere on its own
import json

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

json_string = json.dumps(data)
print(json_string)
# {"name": "John", "age": 30, "active": true}

print(type(json_string))
# <class 'str'>

This is the version most developers reach for first, mainly because a string is easy to pass around into a response body, a log line, or a message queue payload.

json.dump() syntax and file handling

json.dump() takes the same dict, but its second argument is a file-like object, not optional.

import json

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

with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=4)
# returns None; the JSON lands in data.json

It writes JSON straight to that stream instead of building a string first.

Why that matters: for very large dicts, dump() avoids holding the entire JSON string in memory before it hits the disk.

Everything else, indent, sort_keys, separators, works the same way it does in dumps().

Formatting the JSON Output: Indentation, Key Order, and Separators

Raw JSON output from json.dumps() is compact by default, one long line, no spacing.

That's fine for machines. It's rough for humans debugging a payload at 11pm.

Three parameters control how the output looks:

ParameterEffectDefault
indentAdds line breaks and spacing per nesting levelNone, compact
sort_keysOrders dictionary keys alphabeticallyFalse
separatorsSets the item and key separators(', ', ': ')
import json

data = {"zeta": 1, "alpha": {"nested": True}}

print(json.dumps(data))
# {"zeta": 1, "alpha": {"nested": true}}

print(json.dumps(data, indent=4, sort_keys=True))
# {
#     "alpha": {
#         "nested": true
#     },
#     "zeta": 1
# }

print(json.dumps(data, separators=(",", ":")))
# {"zeta":1,"alpha":{"nested":true}}

Setting indent=4 is the fastest way to turn a wall of text into something readable during debugging.

sort_keys is less about readability and more about consistency, useful when diffing two JSON outputs and key order shouldn't create noise that isn't really there.

separators matters most when file size is the concern. Dropping the default spaces with separators=(',', ':') shaves bytes off large payloads, which adds up across thousands of API responses.

None of these change what the data means. They only change how it's laid out on the page.

Converting Nested Dictionaries and Lists of Dicts

json.dumps() doesn't need any special handling for nested structures. It walks through dicts inside dicts, and lists of dicts, automatically.

A configuration object with three or four levels of nesting converts the same way a flat one-level dict does, same function call, same syntax.

  • A dict containing another dict as a value
  • A list where every item is itself a dict
  • Mixed structures: dicts holding lists holding more dicts

Depth isn't unlimited, though.

Python's default recursion limit is 1,000 stack frames, set by the sys module. Deeply nested or self-referencing structures can hit that ceiling before json.dumps() ever finishes.

A dict that references itself, directly or through a chain of nested objects, raises a ValueError with a circular reference message rather than looping forever.

That check runs by default. Turning it off with check_circular=False trades safety for a small speed gain, and it's rarely worth it outside tight performance loops.

Handling Non-Serializable Objects (datetime, Decimal, UUID, and Custom Classes)

json.dumps() only knows how to handle a small set of Python types out of the box.

Everything else raises a TypeError the moment it hits the encoder.

  • datetime and date objects
  • Decimal values
  • UUID instances
  • Instances of custom classes
  • Sets, since JSON has no set type

Two ways exist to fix this, and they solve slightly different problems.

Using the default Parameter

Quick, one-off fix:

Pass a function to the default keyword. json.dumps() calls it whenever it meets an object it can't serialize directly.

import json
from datetime import datetime

def serialize(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj).__name__} is not JSON serializable")

data = {"event": "signup", "created_at": datetime(2026, 9, 5, 14, 30)}

print(json.dumps(data, default=serialize))
# {"event": "signup", "created_at": "2026-09-05T14:30:00"}

For a datetime, that function usually just calls .isoformat() and returns the result as a string.

It's fast to write and fine for a single script or a one-time export.

Building a Custom JSONEncoder Class

For anything reused across a codebase, subclassing JSONEncoder holds up better.

  • Override the default() method once
  • Handle multiple types in the same class: datetime, Decimal, and UUID all in one place
  • Pass it with cls=YourEncoder instead of rewriting a function every time
import json
from datetime import datetime
from decimal import Decimal
from uuid import UUID, uuid4

class RichEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, UUID):
            return str(obj)
        if isinstance(obj, set):
            return sorted(obj)
        return super().default(obj)

data = {
    "id": uuid4(),
    "price": Decimal("19.99"),
    "created_at": datetime(2026, 9, 5),
    "tags": {"python", "json"},
}

print(json.dumps(data, cls=RichEncoder, indent=2))

Third-party libraries take this further. orjson serializes datetime, UUID, and dataclass instances natively, with no custom encoder required, according to its own documentation on GitHub.

That's worth knowing before committing to the standard library for a project that leans heavily on those types.

default parameter: quick to write, works for a single call site, becomes repetitive once the same conversion is needed in multiple places.

Custom JSONEncoder: more setup at the start, but reusable across an entire codebase and keeps every type conversion in one place.

Character Encoding and Unicode in JSON Output

Python 3.9 removed the deprecated encoding parameter from json.loads(), according to the official CPython changelog, closing off an option that had gone unused since Python 3.1.

That change reflects where JSON encoding actually happens today: UTF-8, by default, everywhere.

The part that trips people up isn't the encoding itself. It's ensure_ascii.

ensure_ascii (default True): escapes every non-ASCII character into a \uXXXX sequence, so "café" becomes "caf\u00e9" in the output string.

ensure_ascii=False: leaves those characters as-is, readable as actual text instead of escape codes.

import json

data = {"city": "café", "note": "naïve"}

print(json.dumps(data))
# {"city": "caf\u00e9", "note": "na\u00efve"}

print(json.dumps(data, ensure_ascii=False))
# {"city": "café", "note": "naïve"}

For an API response, the escaped version usually doesn't matter. The client parses it back correctly either way.

For a file someone will open and read, or a dataset with names and text in multiple languages, turning ensure_ascii off makes the output far less painful to look at.

Mixing encodings between the write step and the read step is where most garbled output actually comes from, not from json.dumps() itself.

Converting a Python Dict to a JSON File

Writing a dict straight to a JSON file takes three moving parts: the dict, an open file, and json.dump().

  1. Open the target file in write mode, with UTF-8 encoding set explicitly, using something like open("data.json", "w", encoding="utf-8")
  2. Call json.dump(), passing the dict first and the file object second
  3. Add indent=4 if the file needs to stay human-readable
  4. Close the file, or better, use a with block so it closes automatically
import json

config = {"debug": False, "workers": 4, "hosts": ["a.example", "b.example"]}

with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=4, ensure_ascii=False)

The with statement matters more than it looks like it should.

Without it, an exception partway through writing can leave the file open, or worse, half-written and technically invalid JSON.

If you're testing this in an editor rather than a terminal, running the script directly inside VS Code makes it easy to check the output file the moment it's written.

Common file errors worth knowing:

  • Wrong file path: raises FileNotFoundError before json.dump() ever runs
  • No write permission: raises PermissionError on the open() call
  • Forgetting encoding="utf-8" on Windows: can silently write in the system's default codepage instead

Checking that the file opened correctly before calling json.dump() catches most of these before they turn into a debugging session.

Reversing the Process: Parsing JSON Back into a Python Dict

Every conversion has a return trip. json.loads() and json.load() handle it.

json.loads() reads a JSON string. json.load() reads directly from a file object, the same pairing as dumps() and dump() on the way out.

The type mapping runs in reverse, and it isn't always a perfect mirror of what went in.

JSON typePython type on parse
objectdict
arraylist
stringstr
numberint or float
true / false / nullTrue / False / None
import json

original = {"point": (40.7128, -74.0060), "tags": ["a", "b"]}

text = json.dumps(original)
restored = json.loads(text)

print(restored)
# {'point': [40.7128, -74.006], 'tags': ['a', 'b']}

print(type(original["point"]), type(restored["point"]))
# <class 'tuple'> <class 'list'>

Tuples don't survive the round trip. A Python tuple gets serialized as a JSON array, and parsing that array back always produces a list, never a tuple.

Custom classes have the same problem in reverse. A datetime string comes back as a plain str unless something explicitly converts it, since JSON has no native date type to signal that.

The object_hook parameter can intercept that gap. Passing a function to object_hook lets you rebuild specific keys into richer Python objects the moment json.loads() encounters them.

Validating parsed data before using it matters more than it sounds. A dict that came from an external API can have missing keys, wrong types, or a different shape than expected, and json.loads() won't catch any of that on its own.

Comparing JSON Libraries: json, orjson, ujson, and simplejson

The standard library's json module isn't the only option.

Three third-party libraries come up constantly in this conversation: orjson, ujson, and simplejson.

LibrarySpeed vs. jsonNative datetime/UUID supportInstall requirement
jsonBaselineNo, needs default or a custom encoderBuilt into Python
orjsonRoughly 6x faster on typical payloadsYespip install orjson
ujsonFaster than json, slower than orjsonNopip install ujson
simplejsonClose to json, sometimes slower on nested dataNopip install simplejson

orjson's own benchmark puts json.dumps() at 0.18ms against orjson's 0.03ms serializing the same compact payload, according to the library's documentation on GitHub.

That gap widens further on deeply nested data, which matters more for API responses than flat config files.

Swapping libraries is usually a small code refactoring job rather than a rewrite, since orjson keeps a similar dumps()-style call signature.

simplejson shipped before Python's standard library had its own json module, and some codebases still lean on features it added early, like sort_keys support.

ujson trades some correctness for speed. It has historically been looser about edge cases like very large integers, worth testing before adopting it wholesale.

When each one earns its place:

  • json: small payloads, scripts, anywhere an extra dependency isn't worth it
  • orjson: high-throughput APIs, large nested payloads, datetime-heavy data
  • ujson: a middle ground when orjson's stricter type handling causes friction
  • simplejson: legacy codebases already built around it

Performance and Memory Behavior at Scale

Performance differences that don't matter on a 10-key dict start mattering once payloads grow.

Key figures from orjson's published benchmarks (GitHub, ijl/orjson) show what happens on a heavily nested 489KB JSON fixture:

  • orjson: 0.59ms compact, 0.71ms pretty-printed
  • Standard json module: 4.16ms compact, 33.42ms pretty-printed
  • simplejson: 10.43ms compact, 42.13ms pretty-printed

The gap between compact and pretty-printed output is the detail worth noticing here.

The standard library's pretty-printing cost jumps roughly 8x on this fixture, while orjson's barely moves.

That's the real argument for switching libraries. Not raw speed on a small dict, but what happens once indent=4 gets applied to a large, deeply nested response.

Memory follows a similar pattern. Building a full JSON string in memory, which is what dumps() does, costs more on large payloads than streaming straight to a file with dump().

For logging events, saving config, or serving small API responses, none of this is worth the switch. The standard library holds up fine under normal load.

Fixing Common Errors When Converting Dicts to JSON

Most dict-to-json errors come from a small handful of causes. The traceback usually names the exact object at fault.

Object of type X is not JSON serializable is the message that shows up most often, and it's usually accurate about where to look.

import json

json.dumps({"tags": {"python", "json"}})
# TypeError: Object of type set is not JSON serializable

# Fix: convert to a JSON-safe type first
json.dumps({"tags": sorted({"python", "json"})})
# '{"tags": ["json", "python"]}'

Reading the exact type name in that message saves guesswork. It points straight to where handling needs to be added instead of scanning the whole dict.

Common causes and fixes:

  • Sets used as values: convert to a list before serializing
  • Non-string dict keys like tuples: these raise a TypeError, while int and float keys get silently converted to strings
  • Custom class instances: add a default function, or a custom encoder, before passing the object in
  • bytes objects: decode to a string first, since JSON has no binary type

Circular references produce a different error entirely. A ValueError mentioning circular reference means check_circular caught a structure that would otherwise loop forever.

Silent formatting mistakes are the harder category to catch. A dict with mixed key types can serialize without error, then produce a key order that looks wrong once parsed back somewhere else.

Catching these early usually comes down to solid unit testing around any function that serializes data, not clever error handling after the fact.

Validating the JSON string with json.loads() right after calling dumps() catches most of the rest before they reach production. For a quick check on a payload you already have as text, a string to JSON converter does the same job without writing a script.

Using Dict to JSON Conversion in Web Frameworks

Every major Python web framework wraps json.dumps() somewhere.

That's squarely a back-end development concern. None of these frameworks ask a route handler to call the standard library directly.

Flask and jsonify()

Wraps json.dumps() with the right headers:

  • Sets Content-Type to application/json automatically
  • Returning a plain dict from a route works too, Flask converts it on the way out
  • Handles pretty-printing in debug mode without extra configuration
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/user")
def user():
    return jsonify({"name": "John", "age": 30})
    # Content-Type: application/json, no json.dumps() call needed

Flask's jsonify() has supported decimal.Decimal since version 2.0.2, converting it to a string automatically, according to Flask's own source documentation.

Before that version, a Decimal in the dict meant writing a custom encoder just to avoid a TypeError.

Returning JSON this way is the backbone of most RESTful API endpoints built with Flask.

FastAPI Automatic Serialization

FastAPI serializes return values without a jsonify() equivalent at all.

A dict, a Pydantic model, or a dataclass returned from a path operation gets converted through jsonable_encoder() before it reaches the response.

from fastapi import FastAPI

app = FastAPI()

@app.get("/user")
def user():
    return {"name": "John", "age": 30}
    # serialized automatically, no jsonify() equivalent

Netflix built Dispatch, its open-source crisis-management tool, on top of FastAPI, according to the framework's own documentation of real-world usage.

Declaring a response model changes that path. FastAPI's documentation notes that a response model typically performs better than returning a JSONResponse directly, since data gets serialized through Pydantic's compiled core rather than pure Python.

Django's JsonResponse

Django takes the strictest approach of the three.

safe=True by default:

  • Only dict instances are accepted
  • Passing a list or any other object raises a TypeError unless safe=False is set explicitly
  • DjangoJSONEncoder handles dates, UUIDs, and Decimal out of the box
from django.http import JsonResponse

def user(request):
    return JsonResponse({"name": "John", "age": 30})

def tags(request):
    # a list needs safe=False, or Django raises TypeError
    return JsonResponse(["python", "json"], safe=False)

That default exists for a documented reason. Older browsers had a security issue with top-level JSON arrays, so Django guards against returning one by accident.

When Dict to JSON Conversion Does Not Work

Not every dict converts cleanly, and assuming otherwise causes more damage than the conversion itself.

Types with no safe JSON representation:

  • Open file handles and sockets: there's no meaningful JSON form for an active connection
  • Generators: they get consumed, not structured, and json.dumps() has nothing left to iterate over twice
  • Functions and lambdas: code isn't data, and serializing one would only capture a name at best

Non-string dict keys create a quieter problem. json.dumps() converts integer, float, and boolean keys to strings without warning.

That means {1: "a", "1": "b"} can collapse into a single key once serialized and read back elsewhere.

Python's own json documentation confirms that allow_nan defaults to True, which lets NaN, Infinity, and -Infinity through even though the JSON specification doesn't define them as valid values.

That output parses fine in another Python program. It breaks in a strict parser, including plenty of JavaScript environments, that follows the specification literally.

Conversion succeeding is not the same as conversion being valid everywhere the file ends up.

Where this leaves you:

  • Convert to a JSON-safe type before serializing, not after something downstream fails
  • Set allow_nan=False during development to catch NaN and Infinity early
  • Treat a successful json.dumps() call as necessary, not sufficient, proof that the output is valid

FAQ on Python Dict to JSON Converter

What does serialization mean in python?

Serialization is the process of converting a python object, like a dict or list, into a format that can be stored or transmitted. Deserialization does the opposite. json.dumps() and json.dump() are python's built-in serialization functions for the JSON format specifically.

When should you use simplejson instead of the built-in json module?

simplejson matters mostly on older python versions, since it ships C speedups and features backported before the standard library gets them. On modern python 3.9+, the built-in json module covers nearly everything simplejson offers, so switching rarely pays off.

Does dict key order matter when converting to json?

Since python 3.7, dicts preserve insertion order, and json.dumps() keeps that order by default. JSON itself, per the specification, treats objects as unordered key-value pairs, so downstream parsers aren't required to respect the order you serialized.

What are the most common mistakes when converting dicts to json in python?

A common mistake is using default=str as a catch-all, which silently masks bugs instead of handling types properly. Others include leaving pretty-printing enabled in production APIs, wasting bandwidth, and assuming a successful json.dumps() call means the data validates against a schema.

Can every python dict be converted to json without modification?

No. Any dict holding only strings, numbers, booleans, lists, or nested dicts converts directly. Anything else, like datetime objects, sets, or custom classes, raises a TypeError until a default function or custom encoder handles the conversion first.