To turn a Python dictionary into JSON, use the json module: json.dumps(my_dict) returns a JSON string, and json.dump(my_dict, file) writes it straight to a file. The one-letter difference - dumps with an s for string, dump without for a file object - is the thing to get right. Add indent=2 for readable output, and know up front that not every Python value converts cleanly: datetime, set, and custom objects need a little help. Get those three things straight and dict-to-JSON is a solved problem.
Table of contents
- dumps versus dump
- Pretty printing with indent
- The ensure_ascii trap with non-English text
- The types that will not serialize
- Round-tripping and the key-type gotcha
- How this fits the rest of the stack
- FAQ
dumps versus dump
The two functions differ by one letter and one purpose:
import json
data = {"name": "Sam", "roles": ["admin", "dev"], "active": True}
# dumps -> a JSON string
text = json.dumps(data)
# '{"name": "Sam", "roles": ["admin", "dev"], "active": true}'
# dump -> write to a file object
with open("data.json", "w") as f:
json.dump(data, f)
dumps (string) returns a string you can log, send in a request, or store. dump (no s) writes directly to an open file, which is what you want when the destination is a file on disk - it avoids building the whole string in memory first.
The mnemonic: the s is for string. dumps gives you a string; dump dumps into a file. Their loading counterparts mirror this - loads parses a string, load reads a file. Once the s-for-string rule clicks, you never mix them up again.
Pretty printing with indent
Default output is compact, on one line. For anything a human will read, pass indent:
print(json.dumps(data, indent=2))
{
"name": "Sam",
"roles": [
"admin",
"dev"
],
"active": true
}
indent=2 (or 4) produces the familiar nested layout. Add sort_keys=True to order keys alphabetically, which makes output deterministic - useful for diffs and tests where you want the same dict to always produce byte-identical JSON.
For the opposite need - the most compact possible output, no spaces at all - override the separators:
json.dumps(data, separators=(",", ":")) # no spaces anywhere
Compact for network payloads where every byte counts, indented for config files and logs a person reads. Pick based on the audience: machines get compact, humans get indent.
The ensure_ascii trap with non-English text
By default, json.dumps escapes every non-ASCII character into \uXXXX sequences:
json.dumps({"city": "Munchen"}) # fine
json.dumps({"city": "München"}) # {"city": "München"}
That is valid JSON, but it is unreadable and bloated for any text with accents, non-Latin scripts, or emoji. The fix is ensure_ascii=False:
json.dumps({"city": "München"}, ensure_ascii=False)
# {"city": "München"}
With ensure_ascii=False, characters are written as-is in UTF-8. For any application handling names, addresses, or content in real-world languages, this is almost always what you want - the escaped form is technically correct but practically hostile. When writing to a file, pair it with an explicit encoding: open("data.json", "w", encoding="utf-8"). This one keyword is the difference between readable international text and a wall of ü.
The types that will not serialize
JSON knows a small set of types: objects, arrays, strings, numbers, booleans, null. Common Python values fall outside it and raise TypeError: Object of type X is not JSON serializable:
datetimeanddate- not JSON types.set- JSON has no set; convert to a list.Decimal- not a JSON number by default.- Custom class instances - JSON has no idea how to represent them.
The clean fix is the default argument - a function json.dumps calls for anything it cannot serialize:
from datetime import datetime, date
def convert(o):
if isinstance(o, (datetime, date)):
return o.isoformat()
if isinstance(o, set):
return list(o)
raise TypeError(f"not serializable: {type(o)}")
json.dumps({"when": datetime.now(), "tags": {"a", "b"}}, default=convert)
default receives each unserializable object and returns something JSON can handle - an ISO string for a date, a list for a set. This is the standard, extensible way to teach json about your own types, far better than pre-converting the whole dict by hand.
Round-tripping and the key-type gotcha
JSON only allows string keys. If your dict has non-string keys, json silently converts them to strings, and they come back as strings:
d = {1: "a", 2: "b"}
text = json.dumps(d) # '{"1": "a", "2": "b"}' - keys are now strings
back = json.loads(text) # {'1': 'a', '2': 'b'} - NOT the integers you started with
The integer keys became string keys, and a round trip does not restore them. If you rely on integer keys, this is a real bug - the data looks the same but the keys changed type. Either use string keys throughout, or convert back explicitly after loading.
More broadly, JSON is lossy for Python: tuples become lists, integer keys become strings, and any custom type had to be flattened. If you need a perfectly faithful round trip of arbitrary Python objects, JSON is the wrong tool - but for interchange with other systems, which is what JSON is for, these constraints are the price of a format everything can read.
How this fits the rest of the stack
Serializing data to JSON correctly - readable Unicode, dates that survive the trip, keys that mean what you think - is the contract every API speaks. When your Python is the service returning that JSON to other systems, the encoding details stop being cosmetic and become the difference between a clean integration and a support ticket. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.
Useful related references:
- Python Append to List: append, extend, and the Default-Argument Trap
- Python Environment Variables: os.environ, .env Files, and pydantic
- Python or: Short-Circuits, Truthiness, and the Default-Value Trap
- Python services on RunxBuild
FAQ
How do I convert a Python dict to JSON?
Use the json module: json.dumps(my_dict) returns a JSON string, and json.dump(my_dict, file) writes JSON directly to an open file. The s in dumps stands for string; dump without the s writes to a file object.
What is the difference between json.dump and json.dumps?
json.dumps returns a JSON string in memory, which you can log, send, or store. json.dump writes JSON straight to an open file object without building the full string first. Use dumps for a string result and dump when the destination is a file.
Why does json.dumps turn accented characters into \u codes?
Because ensure_ascii defaults to True, escaping non-ASCII characters into \uXXXX sequences. Pass ensure_ascii=False to keep characters like ü as-is in UTF-8, which is almost always what you want for real-world text. Pair it with encoding="utf-8" when writing to a file.
Why do I get Object of type datetime is not JSON serializable?
Because datetime, set, Decimal, and custom objects are not JSON types. Pass a default function to json.dumps that converts them - for example returning o.isoformat() for dates and list(o) for sets - and json will call it for anything it cannot serialize on its own.
Does converting a dict to JSON and back preserve integer keys?
No. JSON only allows string keys, so integer keys are converted to strings during serialization and come back as strings after json.loads. If you depend on integer keys, use string keys throughout or convert them back explicitly after loading.