Structured Data and SerializationIntroductionMost useful programs exchange data with other programs, store information between executions, or move information across networks. A Python application may read customer records from a CSV file, load settings from JSON, preserve a trained model, or communicate with a Java service. These activities depend on structured data: data organised according to identifiable fields, types, and relationships. Serialization is the process of converting an in-memory object or data structure into a storable or transferable representation. Deserialization reverses that process. The representation may be text-based, such as CSV or JSON, or binary, such as a database page, protocol buffer, or Python pickle stream. Choosing a representation is not merely a programming convenience. It affects interoperability, precision, storage size, performance, validation, long-term maintainability, and security. This chapter develops practical techniques using Python’s standard library and provides criteria for selecting an appropriate format. Learning ObjectivesBy the end of this chapter, you should be able to: - read and write CSV data reliably using
csv; - encode and decode JSON using
json; - design configuration files that are clear, validated, and maintainable;
- distinguish text serialization from binary serialization;
- explain the strengths and limitations of
pickle; - preserve schema consistency and handle missing, extra, or incorrectly typed fields;
- evaluate interoperability, precision, performance, and security risks;
- construct a robust data-processing workflow using standard Python libraries.
1. Data Representation and SerializationAn in-memory Python object has a type, structure, and behaviour. For example: record = {
"student_id": 1042,
"name": "Amina Khan",
"active": True,
"modules": ["Python", "Databases"]
}This dictionary is convenient inside Python, but another program cannot automatically access Python’s memory. Serialization creates an external representation, such as: {"student_id": 1042, "name": "Amina Khan", "active": true, "modules": ["Python", "Databases"]}A useful serialization design should answer five questions: - What is the schema? Which fields exist, and what types do they have?
- Who will consume the data? Python, Java, a spreadsheet, a person, or a web service?
- How durable must the representation be? Is it temporary or an archival format?
- What precision is required? Are monetary values, dates, or scientific measurements involved?
- What threats exist? Could an attacker control or modify the serialized data?
A format is not automatically superior because it is compact or convenient. A human-readable format may be preferable for configuration, while a binary format may be suitable for controlled internal caching. 2. CSV: Tabular Data for InterchangeCSV, or comma-separated values, represents a table as rows and columns. The first row often contains column headings. CSV is common because it is supported by spreadsheet applications, databases, statistical tools, and programming languages. A simple file might be: student_id,name,mark
101,Amina Khan,82
102,Jon Bell,74 CSV has no universal complete standard in practice. Variations include delimiters, quoting rules, line endings, character encodings, and whether a header exists. Python’s csv module handles these details more safely than manual string splitting. Reading CSV with dictionariesimport csv
with open("marks.csv", "r", newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
for row in reader:
student_id = int(row["student_id"])
mark = float(row["mark"])
print(student_id, row["name"], mark)DictReader maps column headings to values. However, every value initially arrives as a string. Conversion must therefore be explicit and validated.
Writing CSVimport csv
records = [
{"student_id": 101, "name": "Amina Khan", "mark": 82},
{"student_id": 102, "name": "Jon Bell", "mark": 74},
]
with open("marks.csv", "w", newline="", encoding="utf-8") as file:
fieldnames = ["student_id", "name", "mark"]
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(records)The newline="" argument is recommended by the documentation for the csv module because it allows the module to manage newline translation correctly on different platforms. Quoting and dialectsFields containing commas, quotation marks, or line breaks must be quoted. The module handles this automatically: import csv
with open("people.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(["name", "comment"])
writer.writerow(["Lee", "Excellent, reliable work"])For semicolon-separated files, specify a delimiter: reader = csv.DictReader(file, delimiter=";") CSV limitationsCSV does not intrinsically record types. The string 2025-04-01 may be a date, identifier, or ordinary text. It also has weak support for nested objects, null values, and relationships. Two systems may interpret an empty field differently: as missing, empty text, or zero. CSV is therefore appropriate for flat tables and broad human interoperability, but not for complex hierarchical structures or strict self-describing data. 3. JSON: Structured, Interoperable TextJSON (JavaScript Object Notation) represents objects, arrays, strings, numbers, Boolean values, and null. Python’s corresponding values are dictionaries, lists, strings, integers or floating-point numbers, True and False, and None. | JSON value | Typical Python value |
|---|
| object | dict | | array | list | | string | str | | number | int or float | true / false | True / False | null | None |
Encoding and decodingimport json
record = {
"student_id": 101,
"name": "Amina Khan",
"active": True,
"modules": ["Python", "Databases"]
}
with open("student.json", "w", encoding="utf-8") as file:
json.dump(record, file, indent=2)
with open("student.json", "r", encoding="utf-8") as file:
loaded = json.load(file)dump and load operate on files. dumps and loads operate on strings:
text = json.dumps(record)
copy = json.loads(text) Indentation improves readability but increases file size. For machine-to-machine transfer, compact output may be preferable. JSON and custom typesJSON does not natively represent Python sets, tuples, dates, decimals, or arbitrary class instances. A common solution is to define an explicit representation: from datetime import date
import json
item = {
"description": "Subscription",
"start_date": date.today().isoformat()
}
text = json.dumps(item)The ISO 8601 date representation is readable and widely interoperable. On reading, the application can validate and convert it back to a date object. Validation and schema consistencySuccessful JSON parsing does not prove that the data is acceptable. This parses successfully: {"student_id": "unknown", "mark": -20}A reliable application performs structural and semantic validation: def validate_mark_record(record):
required = {"student_id", "name", "mark"}
missing = required - record.keys()
if missing:
raise ValueError(f"Missing fields: {sorted(missing)}")
if not isinstance(record["student_id"], int):
raise TypeError("student_id must be an integer")
if not isinstance(record["name"], str) or not record["name"].strip():
raise ValueError("name must be non-empty text")
if not isinstance(record["mark"], (int, float)):
raise TypeError("mark must be numeric")
if not 0 <= record["mark"] <= 100:
raise ValueError("mark must be between 0 and 100")A production system should also decide whether unknown fields are rejected, ignored, or preserved. Rejecting them detects mistakes early; allowing them can support forward compatibility. The policy should be documented rather than accidental. JSON numbers and precisionJSON specifies numbers but does not require every implementation to use the same numeric representation. Python normally decodes JSON numbers into int or float. Binary floating-point cannot represent every decimal fraction exactly. For example, a value such as 0.1 may be stored as the nearest representable binary fraction. For financial values, storing a decimal amount as a string or as an integer number of minor units is often safer: {"amount_minor": 1099, "currency": "GBP"}Here, 1099 means 1,099 pence, provided the schema explicitly states the unit. Alternatively, Python’s decimal.Decimal can be used with a controlled conversion process. Precision requirements must be agreed by all systems, not solved only at the file-writing stage. 4. Configuration FilesConfiguration separates changeable deployment choices from program code. Examples include database addresses, logging levels, feature flags, and file locations. JSON is suitable for many applications because it is standard, readable, and available without third-party packages. Example configuration: {
"service": {
"host": "localhost",
"port": 8080
},
"logging": {
"level": "INFO"
},
"features": {
"new_report": false
}
}A robust configuration loader should distinguish at least three failures: FileNotFoundError: the configuration file is absent;json.JSONDecodeError: the file is not valid JSON;KeyError, TypeError, or ValueError: the structure or values are invalid.
import json
DEFAULTS = {"host": "localhost", "port": 8000, "log_level": "INFO"}
def load_config(path):
try:
with open(path, encoding="utf-8") as file:
data = json.load(file)
except FileNotFoundError:
return DEFAULTS.copy()
except json.JSONDecodeError as error:
raise ValueError(f"Invalid configuration syntax: {error}") from error
service = data.get("service", {})
host = service.get("host", DEFAULTS["host"])
port = service.get("port", DEFAULTS["port"])
if not isinstance(host, str):
raise TypeError("service.host must be text")
if not isinstance(port, int) or not 1 <= port <= 65535:
raise ValueError("service.port must be an integer from 1 to 65535")
return {"host": host, "port": port}Configuration should not normally contain passwords or private keys in a repository. Use environment variables, an operating-system secret store, or a dedicated secrets-management system. File permissions should restrict access to sensitive configuration. 5. Binary Data and the pickle ModuleBinary data is represented as bytes rather than ordinary text. Images, compressed files, executables, and many network protocols are binary. Python provides the bytes type and functions such as open(..., "rb") and open(..., "wb"). with open("input.bin", "rb") as source:
data = source.read()
with open("copy.bin", "wb") as destination:
destination.write(data)Picklepickle serializes many Python objects into a Python-specific binary stream:
import pickle
model_state = {"weights": [0.2, 0.8], "name": "demo"}
with open("state.pkl", "wb") as file:
pickle.dump(model_state, file)
with open("state.pkl", "rb") as file:
restored = pickle.load(file)Pickle can preserve object graphs, shared references, and many Python-specific types. It is useful for controlled temporary storage, testing, and communication between trusted Python processes using compatible code versions. However, unpickling is not merely data parsing. A malicious pickle can cause code execution during loading. Never unpickle data received from an untrusted user, public download, email attachment, or unverified service. Authentication, file permissions, and a trusted origin do not make arbitrary external pickle data automatically safe; the loader must still have a justified trust boundary. Pickle is also Python-specific and can be fragile across versions, class changes, and package changes. It is usually unsuitable for public APIs, long-term archives, or interoperability with Java and non-Python systems. JSON, CSV, or a deliberately specified binary protocol is normally more appropriate for those purposes. 6. Serialization Choices: A Decision FrameworkThe following comparison supports practical selection. | Criterion | CSV | JSON | Pickle | Raw binary/custom format |
|---|
| Human readability | High | High | Low | Usually low | | Tabular data | Excellent | Adequate | Possible | Possible | | Nested data | Poor | Excellent | Excellent | Depends on design | | Non-Python interoperability | Excellent | Excellent | Poor | Depends on specification | | Type preservation | Weak | Moderate | Strong for Python objects | Depends on design | | Security when untrusted | Generally safer, still validate | Generally safer, still validate | Unsafe to load blindly | Depends on parser | | Long-term stability | Good with documented schema | Good with versioned schema | Limited | Depends on documentation |
A decision can be expressed as a sequence of questions: - Is the data a simple table intended for spreadsheets or many languages? Choose CSV.
- Is the data hierarchical or intended for web and service integration? Choose JSON.
- Must Python-specific object structure be retained, and is the source fully trusted? Pickle may be suitable.
- Are extreme performance, compactness, or specialised types required? Define or adopt a documented binary protocol.
The final choice should also consider file size, processing speed, schema evolution, audit requirements, and the cost of recovering from corruption. 7. Robust File-Processing PatternsContext managers and encodingAlways use with when opening files so that resources are closed even when an exception occurs. Specify the encoding for text files, usually UTF-8 when interoperating across systems. Atomic replacementWriting directly to an important configuration file can leave a partially written file if the process stops during writing. A safer pattern is to write a temporary file in the same directory, flush and close it, then replace the original using os.replace. import json
import os
import tempfile
def save_json_atomically(path, value):
directory = os.path.dirname(os.path.abspath(path))
fd, temporary = tempfile.mkstemp(dir=directory, prefix=".tmp-")
try:
with os.fdopen(fd, "w", encoding="utf-8") as file:
json.dump(value, file, indent=2)
file.write("\n")
os.replace(temporary, path)
except Exception:
try:
os.unlink(temporary)
except FileNotFoundError:
pass
raiseos.replace provides a stronger update pattern on ordinary local filesystems than overwriting the target in place, although durability guarantees can vary by operating system and storage system.
Exception boundariesCatch exceptions at the level where the program can respond meaningfully. Do not use a broad except Exception merely to hide failures. Report malformed input with context, preserve the original exception using raise ... from error, and avoid logging passwords or personal data. Streaming large filesjson.load reads a complete JSON document into memory. For very large datasets, line-oriented JSON objects, CSV iteration, or a database may be more appropriate. A CSV reader naturally supports streaming:
with open("large.csv", newline="", encoding="utf-8") as file:
for row in csv.DictReader(file):
process(row)A standard JSON document is not generally streamable one object at a time without an agreed framing convention. One possible convention is one JSON object per line, commonly called JSON Lines, but the producer and consumer must explicitly agree on it. 8. Schema Evolution and InteroperabilityA schema is a contract describing fields, types, constraints, units, and meanings. Schema evolution occurs when a contract changes. Adding an optional field is usually less disruptive than renaming or changing the meaning of an existing field. Professional practices include: - include a format or schema version;
- document required and optional fields;
- define units, time zones, and date formats;
- specify whether unknown fields are accepted;
- maintain backward-compatible readers where feasible;
- test representative old and new files;
- avoid relying on dictionary ordering or incidental formatting;
- use stable identifiers rather than display names.
Dates are a frequent interoperability problem. A date-time should specify its time zone or use UTC by agreement. A number should specify its unit and precision. A field named amount is ambiguous unless the schema states currency and scaling. 9. Applied Case: Student Assessment ExchangeThe following is an illustrative case. BackgroundA college stores assessment results in Python. The registry exports marks to a spreadsheet team, while a Java reporting service receives data through a shared directory. The original implementation writes comma-separated strings manually and occasionally produces incorrect columns when a student’s name contains a comma. ProblemThe college needs reliable transfer of student identifiers, module codes, marks, and assessment dates. It must preserve numeric meaning, reject invalid marks, and allow future fields such as moderation status. AnalysisCSV is appropriate for the spreadsheet export because the data is tabular. JSON is more suitable for the Java service because it can represent a record with metadata and a list of assessment components. Pickle is rejected because Java cannot naturally consume it and because a shared directory does not establish a trusted pickle boundary. The team defines the following JSON contract: {
"schema_version": 1,
"student_id": "S1042",
"module_code": "PY401",
"mark": 82,
"assessment_date": "2025-05-14",
"currency": null
}The currency field is shown only to emphasise that irrelevant fields should not be added casually; an actual assessment schema would omit it. The mark is an integer from 0 through 100, and the date uses ISO format. The identifier remains a string because leading zeroes may be meaningful. Approach and solutionThe exporter uses csv.DictWriter for the spreadsheet and json.dump for service records. Both outputs are generated from the same validated internal record. Validation occurs before serialization, so the two formats cannot silently disagree about a malformed mark. The service reader accepts version 1 and rejects unknown schema versions with a clear error. OutcomeNames containing commas are correctly quoted in CSV. Java reads the JSON without depending on Python-specific types. Invalid marks are rejected before publication, and later schema changes can be handled through a versioned contract. Learning pointsThe central design decision was not a choice between fashionable formats. It was matching each representation to its consumer while defining a shared semantic contract. Serialization is successful only when the receiving system interprets values in the intended way. 10. Security, Ethics, and GovernanceSerialized data may contain personal, educational, financial, or operational information. Developers should apply data minimisation: store only what is needed. Access controls, encryption during transfer and at rest, retention limits, and audit trails may be required by organisational policy or data-protection law. Parsing untrusted data can also create denial-of-service risks. Very large JSON documents, deeply nested structures, or intentionally repeated input may consume excessive memory or processing time. Applications should impose appropriate file-size, record-count, and nesting limits where the threat model requires them. Validation is not only a correctness measure. It is a security boundary. Treat external data as untrusted, reject unexpected types, avoid constructing executable code from fields, and never confuse serialization with authentication. A signed file can provide integrity or origin evidence only when signatures and key management are implemented correctly. 11. Contemporary PracticeModern systems increasingly use schema registries, API contracts, versioned event formats, and automated compatibility tests. JSON remains dominant for many web APIs, while specialised binary formats are used when compactness, speed, or strict schemas are important. Python’s standard library is sufficient for CSV, JSON, bytes, and pickle, but larger systems may adopt validated data models, databases, or organisation-wide serialization protocols. Artificial intelligence systems also depend heavily on serialization. A model checkpoint may contain executable or framework-specific state and therefore requires a stronger trust policy than an ordinary JSON prediction record. Model files should be obtained from trusted sources, verified where possible, isolated during loading, and documented with framework and version metadata. Best-Practice Checklist- Define the schema before implementing the writer.
- Use
csv rather than splitting lines manually. - Use UTF-8 and explicit newline handling for text files.
- Validate both structure and meaning after parsing.
- Document dates, time zones, units, precision, and missing values.
- Version formats that will evolve.
- Use atomic replacement for important files.
- Stream large tabular files where practical.
- Keep secrets out of ordinary configuration files.
- Never unpickle untrusted data.
- Choose interoperability and longevity over convenience when data leaves Python.
- Test malformed, incomplete, extra-field, and legacy inputs.
Chapter SummaryStructured data allows applications to store and exchange information in a predictable form. CSV is effective for flat tables and broad tool compatibility, but it provides little type information. JSON supports hierarchical, readable, language-independent data, yet still requires explicit validation and careful treatment of numbers, dates, and custom types. Configuration files should be loaded with clear exception handling, defaults, validation, and secure handling of secrets. Binary data is represented as bytes, while pickle preserves many Python-specific objects. Pickle is convenient only within a trusted and controlled Python environment; unpickling untrusted content can execute malicious code. Serialization choices should be evaluated against schema clarity, interoperability, precision, performance, evolution, and security. Professional implementations use context managers, explicit encodings, atomic updates, streaming where suitable, and documented versioned contracts. Key TermsSerialization: Conversion of an in-memory object into a storable or transferable representation. Deserialization: Reconstruction of an in-memory value from serialized data. Schema: A documented description of fields, types, constraints, units, and relationships. CSV: A text format representing tabular rows and columns. Delimiter: A character separating fields in a tabular text format. Quoting: Enclosing field content so delimiters or line breaks are treated as data. JSON: A text-based format for objects, arrays, strings, numbers, Booleans, and null. Interoperability: The ability of different systems to exchange and interpret data. Validation: Checking that input has acceptable structure, types, and values. Schema evolution: Modification of a data contract over time. Precision: The degree to which a stored value represents the intended value. Binary data: Data represented as bytes rather than ordinary text. Pickle: Python-specific binary serialization supporting many Python object types. Trust boundary: A point at which data changes from trusted to untrusted or vice versa. Atomic replacement: Updating a file by preparing a complete temporary file and replacing the target. Encoding: The mapping between characters and bytes. Streaming: Processing data incrementally rather than loading it all into memory. Configuration: External settings controlling application behaviour. JSON Lines: A convention in which each line contains a separate JSON value, usually an object. Data minimisation: Storing or processing no more personal or sensitive data than necessary. Review QuestionsShort Answer Questions- Why should CSV values be explicitly converted after reading?
- What is the difference between
json.dump and json.dumps? - Why is
pickle.load dangerous for untrusted input? - Give two examples of information that a schema should document besides field names.
- What problem does atomic file replacement help prevent?
Long Answer Questions- Compare CSV and JSON for a data exchange between a spreadsheet user and a web service.
- Explain how a robust configuration loader should handle missing files, invalid syntax, and invalid values.
- Discuss the effects of floating-point representation on serialized numeric data and recommend a strategy for monetary amounts.
- Explain schema evolution and describe practices that support backward compatibility.
- Evaluate the advantages and limitations of
pickle for temporary storage, public APIs, and long-term archives.
Analytical / Application Questions- Design a serialization strategy for a Python application that exports flat sales data to analysts and nested order data to a Java service. Justify the formats, validation rules, and versioning policy.
- A JSON file contains a valid object but uses a string for an identifier, a negative quantity, an ambiguous local date, and an unknown field. Analyse which issues are syntactic and which are semantic, then propose a validation response.
- An organisation receives model files from an external supplier. Assess whether loading them with
pickle is appropriate and design a safer trust, verification, and isolation process.
Multiple Choice Questions with AnswersQuestion 1Which Python class is most appropriate for reading CSV rows using column names? A. csv.DictReader B. json.JSONDecoder C. pickle.Unpickler D. io.BytesIO Correct Answer: A. csv.DictReader Question 2What does json.dumps() return? A. A Python dictionary B. A JSON-formatted string C. A binary pickle stream D. A file object Correct Answer: B. A JSON-formatted string Question 3Why must JSON data be validated after successful parsing? A. Parsing automatically converts all values to integers B. Valid JSON syntax does not guarantee correct types or business values C. JSON cannot contain objects D. Validation is required only for CSV Correct Answer: B. Valid JSON syntax does not guarantee correct types or business values Question 4Which format is generally most suitable for a flat table intended for spreadsheet users? A. CSV B. Pickle C. Python bytecode D. An arbitrary memory dump Correct Answer: A. CSV Question 5What is the principal security risk of loading an untrusted pickle file? A. It always loses Unicode characters B. It cannot represent dictionaries C. It may execute malicious code during loading D. It changes every integer into a float Correct Answer: C. It may execute malicious code during loading Question 6Which technique best supports safe replacement of an important configuration file? A. Overwrite it one character at a time B. Write a temporary complete file and use os.replace C. Convert it to CSV first D. Load it with pickle Correct Answer: B. Write a temporary complete file and use os.replace Question 7What does csv.DictReader initially return for ordinary field values? A. Strings B. Only integers C. Python dates D. Bytecode instructions Correct Answer: A. Strings Question 8Which representation is usually best for a Python-specific object graph exchanged only between trusted compatible Python processes? A. Pickle B. CSV C. Plain text with no schema D. An HTML table Correct Answer: A. Pickle Question 9Why should a monetary JSON value include a unit or currency definition? A. JSON cannot store numbers B. The meaning and scale of an amount are otherwise ambiguous C. Currency makes JSON executable D. It prevents all file corruption Correct Answer: B. The meaning and scale of an amount are otherwise ambiguous Question 10Which practice most directly improves interoperability across programming languages? A. Relying on private Python class names B. Using a documented, versioned schema with explicit types and units C. Serializing arbitrary memory addresses D. Omitting field names to reduce size Correct Answer: B. Using a documented, versioned schema with explicit types and units
|