json.dumps serializes a Python object to a JSON string; json.loads
parses a JSON string back into Python objects. The dump/load variants
(no s) work with file objects instead. The conversion follows a fixed type
mapping.
import json
data = {"name": "Ada", "age": 36, "tags": ["math"], "active": True}
s = json.dumps(data) # dict -> JSON string
back = json.loads(s) # JSON string -> dict
with open("out.json", "w") as f:
json.dump(data, f) # write to file
The mapping: dict->object, list/tuple->array, str->string, int/float
->number, True/False->true/false, None->null. Note tuples become
arrays (you get a list back), and dict keys are coerced to strings. JSON has
no native date, set, or bytes type.
Unsupported types raise TypeError: ... is not JSON serializable. The two standard
fixes are the default= callback (a function called for any unserializable
object, returning a JSON-friendly substitute) or a custom JSONEncoder subclass
passed via cls=.
import json
from datetime import datetime
def encode(obj):
if isinstance(obj, datetime):
return obj.isoformat() # turn it into a string
raise TypeError(f"not serializable: {type(obj)}")
json.dumps({"when": datetime.now()}, default=encode)
class MyEncoder(json.JSONEncoder): # the class-based alternative
def default(self, obj):
if isinstance(obj, set):
return list(obj)
return super().default(obj)
json.dumps({1, 2, 3}, cls=MyEncoder)
Use default= for a quick one-off; subclass JSONEncoder when you want reusable
encoding logic. On the way back, use object_hook in loads to reconstruct custom
types.
Both read CSV files, but the row format differs. csv.reader yields each row as
a list of strings indexed by position. csv.DictReader treats the first row
as headers and yields each row as a dict keyed by column name — far more
readable and robust to column reordering.
import csv
with open("people.csv", newline="") as f:
for row in csv.reader(f):
print(row[0], row[1]) # positional — brittle
with open("people.csv", newline="") as f:
for row in csv.DictReader(f):
print(row["name"], row["age"]) # by header — clear
Always open CSV files with newline="" to let the csv module handle line
endings correctly. Prefer DictReader/DictWriter for named-column access; use the
plain reader for headerless or purely positional data.
json is a text, language-independent format for simple data (dicts,
lists, numbers, strings). pickle is a binary, Python-specific format that
can serialize almost any Python object (custom classes, functions references,
nested objects). The crucial caveat: never unpickle untrusted data.
import json, pickle
json.dumps({"x": 1}) # "{\"x\": 1}" — readable, portable
pickle.dumps({"x": 1}) # b'\x80\x04...' — binary, Python-only
# DANGER: unpickling runs arbitrary code embedded in the data
pickle.loads(untrusted_bytes) # can execute malicious payloads!
pickle.loads can execute arbitrary code during deserialization, so it's a
remote-code-execution risk on attacker-controlled input. Rule of thumb: use json
for config, APIs, and anything crossing a trust boundary; use pickle only for
trusted, internal Python-to-Python data (e.g. caches you wrote yourself).
JSON has no date type, so a datetime must be converted to a string — the
ISO 8601 format via .isoformat() is the standard choice because it's
unambiguous and parseable. On the way out use default=; on the way back parse the
string with datetime.fromisoformat().
import json
from datetime import datetime
event = {"name": "launch", "at": datetime(2026, 6, 18, 14, 30)}
text = json.dumps(event, default=lambda o: o.isoformat())
# '{"name": "launch", "at": "2026-06-18T14:30:00"}'
raw = json.loads(text)
raw["at"] = datetime.fromisoformat(raw["at"]) # back to datetime
JSON can't tell a date-string from an ordinary string, so you must know which fields
to re-parse (or use object_hook). Prefer ISO strings in UTC for portability,
and convert to local time only at display.
json.dumps accepts indent (pretty-print), sort_keys (deterministic key
order), separators (compact output), and ensure_ascii (escape non-ASCII or
keep Unicode). These shape readability and size.
import json
data = {"b": 1, "a": [1, 2]}
json.dumps(data, indent=2) # pretty, multi-line
json.dumps(data, sort_keys=True) # '{"a": [1, 2], "b": 1}'
json.dumps(data, separators=(",", ":")) # '{"b":1,"a":[1,2]}' — compact
json.dumps({"x": "café"}, ensure_ascii=False) # keeps 'café' (not \u...)
Rule of thumb: indent for human-readable files/logs, compact separators for
network payloads, sort_keys for reproducible output (e.g. diffs/hashing).
json.loads(..., object_hook=fn) calls fn on every decoded object (dict),
letting you transform it into a richer Python type — the inverse of default= for
encoding.
import json
from datetime import datetime
def decode(d):
if "at" in d:
d["at"] = datetime.fromisoformat(d["at"])
return d
json.loads('{"at": "2026-06-18T14:30:00"}', object_hook=decode)
# {'at': datetime(2026, 6, 18, 14, 30)}
Rule of thumb: pair default= (encode) with object_hook (decode) to round-trip
custom types like datetimes, Decimals, or your own classes through JSON.
Use csv.writer (lists) or csv.DictWriter (dicts, with writeheader()). Open with
newline="" to prevent blank rows on Windows, and let the module handle quoting
of fields containing commas/quotes/newlines.
import csv
with open("out.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["name", "age"])
w.writeheader()
w.writerow({"name": "Ada, the", "age": 36}) # comma auto-quoted
w.writerows([{"name": "Bob", "age": 40}])
Rule of thumb: always newline="" when opening CSV files, and use DictWriter with
writeheader() for named columns — never hand-build CSV with string joins (quoting!).
Python's json emits NaN, Infinity, -Infinity by default (which is invalid
JSON); pass allow_nan=False to raise instead. Large ints serialize fine but may
lose precision when read by JavaScript (its numbers are 64-bit floats).
import json
json.dumps(float("nan")) # 'NaN' — not valid standard JSON
json.dumps(float("inf"), allow_nan=False) # ValueError
json.dumps(2**60) # fine in Python
# but JS parsing loses precision beyond 2**53
Rule of thumb: set allow_nan=False for strict JSON; serialize very large integers as
strings if a JavaScript/other consumer must read them safely.
Pickle has versioned protocols (higher = more efficient/compact; protocol 5 is
latest). Some objects can't be pickled: open file handles, sockets, lambdas, and
most local/nested functions. __reduce__/__getstate__ customize pickling.
import pickle
pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
pickle.dumps(lambda x: x) # PicklingError — lambdas can't be pickled
pickle.dumps(open("f")) # TypeError — file handles can't be pickled
Rule of thumb: use HIGHEST_PROTOCOL for internal pickling; for things pickle can't
handle (lambdas, connections), redesign or implement __getstate__/__setstate__.
For untrusted or cross-language data, prefer JSON (simple data), json/CSV for
tabular, or schema-based formats like Protocol Buffers, MessagePack, or
TOML/YAML for config. These don't execute code on load.
import json
# safe round-trip for plain data:
json.dump(config, open("config.json", "w"))
# for binary efficiency without code execution: msgpack (third-party)
# for typed cross-language schemas: protobuf, avro
Rule of thumb: reach for pickle only for trusted internal Python state; use JSON/ msgpack/protobuf at trust boundaries and for interoperability.
JSON object keys must be strings, so json.dumps coerces int/float/bool/
None keys to strings — and on loads they come back as strings, not their
original type. Other key types (tuples) raise TypeError.
import json
s = json.dumps({1: "a", True: "b"})
s # '{"1": "a", "true": "b"}' — keys stringified
json.loads(s) # {'1': 'a', 'true': 'b'} — keys are now strings
json.dumps({(1, 2): "x"}) # TypeError — tuple keys not allowed
Rule of thumb: JSON keys round-trip as strings — if you need int keys back, convert them yourself after loading, and avoid non-stringifiable key types.
Pass delimiter=, quotechar=, and quoting= to reader/writer, or
use a registered dialect. This handles TSV, semicolon-separated, or
quote-everything formats.
import csv
# tab-separated:
csv.reader(f, delimiter="\t")
# semicolon with custom quote:
csv.writer(f, delimiter=";", quotechar="'", quoting=csv.QUOTE_ALL)
csv.register_dialect("pipes", delimiter="|")
csv.reader(f, dialect="pipes")
Rule of thumb: configure delimiter/quoting for non-CSV-comma formats; the csv
module handles embedded delimiters/quotes correctly so you don't have to.
json.load reads the whole document into memory. For huge files, use line-delimited
JSON (one object per line) and process line by line, or a streaming/incremental
parser like the third-party ijson.
import json
# JSON Lines: one JSON object per line — stream it
with open("events.jsonl") as f:
for line in f:
obj = json.loads(line) # constant memory per record
process(obj)
# for a single giant nested JSON, use ijson (incremental) instead of json.load
Rule of thumb: prefer JSON Lines for large datasets so you can stream records; reach
for ijson when you must incrementally parse one massive JSON document.
Convert with dataclasses.asdict() to a plain dict for json.dumps, and
reconstruct by unpacking the loaded dict into the dataclass (Cls(**data)). Nested
dataclasses need recursive handling or a library like pydantic.
import json
from dataclasses import dataclass, asdict
@dataclass
class User:
name: str
age: int
u = User("Ada", 36)
s = json.dumps(asdict(u)) # '{"name": "Ada", "age": 36}'
User(**json.loads(s)) # User(name='Ada', age=36)
Rule of thumb: asdict out, Cls(**d) in for flat dataclasses; for nested or
validated models, use pydantic/dataclasses-json rather than hand-rolling.
More Standard Library Essentials interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.