Skip to content

Serialization and Data Formats

The json module is the standard way to serialize Python objects to JSON and back. It ships with CPython and uses a C extension for performance.

import json
data = {
"host": "db-primary",
"port": 5432,
"replicas": ["db-replica-1", "db-replica-2"],
"config": {"pool_size": 10, "timeout": 30.5},
"read_only": False,
"version": None,
}
## Serialize to string
json_str = json.dumps(data, indent=2, sort_keys=True)
print(json_str)
## Deserialize from string
parsed = json.loads(json_str)
print(parsed["host"]) # db-primary
import json
# Write to file
with open("config.json", "w") as f:
json.dump(data, f, indent=2)
# Read from file
with open("config.json") as f:
config = json.load(f)

json.dumps accepts a default parameter for objects that are not JSON-serializable by default:

import json
from datetime import datetime, timezone
from decimal import Decimal
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return list(obj)
if isinstance(obj, bytes):
return obj.decode("utf-8", errors="replace")
return super().default(obj)
data = {
"created": datetime(2025, 1, 15, 10, 30, tzinfo=timezone.utc),
"price": Decimal("19.99"),
"tags": {"python", "json"},
}
json_str = json.dumps(data, cls=CustomEncoder, indent=2)
print(json_str)
import json
from datetime import datetime
def custom_decoder(dct):
if "created" in dct and isinstance(dct["created"], str):
dct["created"] = datetime.fromisoformat(dct["created"])
return dct
json_str = "{"created": "2025-01-15T10:30:00+00:00", "name": "test"}'
data = json.loads(json_str, object_hook=custom_decoder)
print(type(data["created"])) # <class 'datetime.datetime'>
### JSON Performance
import json
import timeit
data = {"key": "value", "numbers": list(range(1000))}
# dumps/load is faster than dump/load for in-memory operations
# C extension is ~10x faster than pure Python fallback
t = timeit.timeit(lambda: json.dumps(data), number=100000)
print(f"dumps: {t:.3f}s for 100000 iterations")

pickle serializes Python objects into a binary format. Unlike JSON, it can serialize almost any Python object, including custom classes, functions, and circular references.

import pickle
class Server:
def __init__(self, host, port, credentials):
self.host = host
self.port = port
self.credentials = credentials
def __repr__(self):
return f"Server({self.host!r}, {self.port})"
srv = Server("db.example.com", 5432, {"user": "admin", "pass": "secret"})
# Serialize
data = pickle.dumps(srv, protocol=pickle.HIGHEST_PROTOCOL)
# Deserialize
srv2 = pickle.loads(data)
print(srv2) # Server('db.example.com', 5432)
print(srv2.credentials) # {'user': "admin'', "pass': "secret''}
ProtocolPython VersionFeatures
0AllASCII, human-readable, backward compatible
11.4Binary format
22.3New-style classes
33.0Bytes objects, no implicit string conversion
43.4Large objects, more types
53.8Out-of-band data, ZSTD compression support
import pickle
print(pickle.DEFAULT_PROTOCOL) # 5 (on Python 3.12)
print(pickle.HIGHEST_PROTOCOL) # 5
# Use highest protocol for best performance and compatibility
data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
### cloudpickle

The standard pickle cannot serialize lambda functions, dynamically defined classes, or objects Defined in __main__. cloudpickle extends pickle to handle these cases:

import cloudpickle
# Standard pickle cannot handle this:
func = lambda x: x ** 2
# pickle.dumps(func) # AttributeError: Can"t pickle local object
# cloudpickle handles it:
data = cloudpickle.dumps(func)
func2 = cloudpickle.loads(data)
print(func2(5)) # 25

cloudpickle is used by distributed computing frameworks like PySpark, Dask, and Ray to ship Closures across processes.

The __reduce__ method controls how an object is pickled:

import pickle
class Config:
def __init__(self, path):
self.path = path
self._data = self._load(path)
def _load(self, path):
return {"loaded_from": path}
def __reduce__(self):
# Return (callable, args) — pickle will call Config(*args)
return (Config, (self.path,))
c = Config("/etc/app/config.yaml")
data = pickle.dumps(c)
c2 = pickle.loads(data)
print(c2._data) # {'loaded_from': "/etc/app/config.yaml''}

YAML is a human-readable data serialization format. Python uses the third-party PyYAML library.

import yaml
config = {
"database": {
"host": "localhost",
"port": 5432,
"name": "production",
},
"logging": {
"level": "INFO",
"handlers": ["stdout", "file"],
},
}
# Serialize
yaml_str = yaml.dump(config, default_flow_style=False, sort_keys=False)
print(yaml_str)
# Deserialize
data = yaml.safe_load(yaml_str)
print(data["database"]["host"]) # localhost
### Custom Tags with Safe Loader
import yaml
# Define a custom constructor that only creates known types
def env_variable_constructor(loader, node):
value = loader.construct_scalar(node)
import os
return os.getenv(value, value)
# Register with a custom SafeLoader
class CustomSafeLoader(yaml.SafeLoader):
pass
CustomSafeLoader.add_constructor("!env", env_variable_constructor)
yaml_str = """
host: !env HOST_NAME
port: !env PORT_NUMBER
"""
data = yaml.load(yaml_str, Loader=CustomSafeLoader)
print(data) # {'host': "localhost'', "port': "localhost''} (or env values)

TOML (Tom”s Obvious Minimal Language) is designed for configuration files. Python 3.11+ includes tomllib in the standard library.

import tomllib # Python 3.11+
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)
print(config["project"]["name"])
print(config["tool"]["pytest"]["testpaths"])
# pip install tomli
import tomli
with open("pyproject.toml", "rb") as f:
config = tomli.load(f)

Python’s built-in tomllib is read-only. For writing, use tomli_w:

# pip install tomli_w
import tomli_w
config = {
"database": {
"host": "localhost",
"port": 5432,
},
"logging": {
"level": "INFO",
},
}
with open("config.toml", "wb") as f:
tomli_w.dump(config, f)
[server]
host = "0.0.0.0"
port = 8080
debug = false
[server.cors]
allowed_origins = ["https://example.com", "https://app.example.com"]
max_age = 3600
[database]
host = "localhost"
port = 5432
name = "myapp"
[[users]]
name = "admin"
role = "superuser"
[[users]]
name = "viewer"
role = "readonly"
## CSV

The csv module handles reading and writing CSV files.

import csv
# Basic reading
with open("data.csv", newline="") as f:
reader = csv.reader(f)
headers = next(reader)
for row in reader:
print(dict(zip(headers, row)))
# DictReader — recommended for most use cases
with open("data.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["email"])
import csv
headers = ["name", "email", "role"]
rows = [
{"name": "Alice", "email": "alice@example.com", "role": "admin"},
{"name": "Bob", "email": "bob@example.com", "role": "user"},
]
with open("output.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(rows)
import csv
# Control quoting behavior
with open("quoted.csv", "w", newline="") as f:
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
writer.writerow(["name", "description"])
writer.writerow(['Alice', 'Has a "title" and, commas'])
# csv.QUOTE_MINIMAL (default) — quote only when necessary
# csv.QUOTE_ALL — quote everything
# csv.QUOTE_NONNUMERIC — quote non-numeric values
# csv.QUOTE_NONE — never quote (raises error if quoting needed)
## Cross-References
  • Essential Modules: Covers the core Python modules that provide foundational functionality, including data structures used in serialization.
  • File I/O: Shows how to read and write files, which is essential for saving and loading serialized data.
  • CLI Tools: Demonstrates how to use command-line interfaces to process and transform serialized data.
  • Data Validation: Explains how to validate serialized data using Pydantic models and other validation techniques.