Module 10 · Capstone Project Milestones ⏱ 18 min

Milestone 5: Formatting & JSON Export

By the end of this lesson you will be able to:
  • Aggregate scattered analysis results into a structured report dict
  • Serialize Python objects to JSON using json.dumps with pretty-printing
  • Convert non-serializable types before serialization to avoid TypeError

Up to now your text analyzer has produced separate pieces: a list of words, a count of frequencies, a handful of extracted emails and dates. Each piece is useful on its own, but a real tool needs to present them as one coherent result. That is what this milestone is for: taking scattered findings and shaping them into a structured report.

A report in Python is simply a dictionary with a clear layout. You choose the keys — summary, statistics, entities — and nest lists and dicts underneath. Once the structure is right, json.dumps turns the whole tree into a single string that any other program can read.

flowchart LR
  D["Python dict"] --> J["json.dumps"]
  J --> S["JSON string"]
  style J fill:#3776ab,color:#fff
json.dumps transforms a Python dict into a portable JSON string.

Building the report dict

A good report has three qualities: it is complete, it is organised, and it is predictable. A caller should know exactly where to find the word count without reading your source code.

The usual shape is a top-level dict with a few fixed keys. summary holds the high-level facts: total words, unique words, line count. statistics holds deeper measurements: average word length, most frequent words, reading-level score. entities holds the structured extractions from the previous milestone: emails, dates, or any other patterns you detected.

Using plain dicts and lists keeps the report serializable. Every value must be something json.dumps understands: strings, numbers, booleans, lists, and dicts. If you slip a set or a custom object in, the conversion will crash with a TypeError.

Build a report dict and serialize it to a compact JSON string.
import json

report = {
    'summary': {'total_words': 1200, 'unique_words': 340},
    'entities': {'emails': ['ada@example.com'], 'dates': ['2024-03-15']}
}

print(json.dumps(report))

Pretty printing for humans

json.dumps produces one long line by default. That is efficient for machines but hard to read during development. The indent parameter adds line breaks and spaces so nested structures open up like a folder tree.

json.dumps(report, indent=2) turns a cramped dict into something you can skim in seconds. The number you pass controls how many spaces each level is indented by. Two is the convention; four is fine if the structure is very deep.

There is a small cost: indented output is larger because of the extra whitespace. For a file saved to disk or sent over a network, you might prefer compact output. For a report displayed in a terminal or a lesson, indentation is almost always worth it.

flowchart TD
  R["Report dict"] --> S1["summary"]
  R --> S2["statistics"]
  R --> S3["entities"]
  style R fill:#3776ab,color:#fff
A well-structured report nests summary, statistics, and entities under clear keys.
The same report, indented so humans can read it.
import json

report = {
    'summary': {'total_words': 1200, 'unique_words': 340},
    'entities': {'emails': ['ada@example.com'], 'dates': ['2024-03-15']}
}

print(json.dumps(report, indent=2))

The serializable trap

Not every Python object can become JSON. Sets, tuples, and custom classes have no direct JSON equivalent, so json.dumps raises a TypeError the moment it meets one. The error message tells you which type offended, but it does not tell you where in a large nested structure the problem lives.

The defensive fix is to convert before you serialize. Turn sets into lists with list(my_set). Replace tuples with lists. For custom objects, build a plain dict of their attributes first. Some programmers use the default parameter of json.dumps to teach it new types, but that is an advanced technique; for now, explicit conversion keeps your reports simple and predictable.

A subtler trap is the datetime object. It looks like a string but is not, so json.dumps rejects it. Convert with .isoformat() before adding it to your report.

flowchart TD
  A["Python set {1, 2}"] --> B["json.dumps"]
  B --> C["TypeError"]
  style C fill:#b91c1c,color:#fff
Passing a set to json.dumps raises TypeError because sets are not JSON-serializable.

Assembling the pipeline

In the completed project, your analyzer runs in stages: clean the text, count the words, extract the entities, and finally build the report. Each stage feeds the next through function calls and return values. The report stage is where everything converges.

Keep the report builder separate from the analysis logic. A function named build_report(stats, entities) takes the outputs of earlier stages and formats them. That separation means you can change the report layout without touching the counting code, and you can test the formatter with fake data.

Exercise

Write word_counts_to_json(counts) that takes a dict like {'hello': 3, 'world': 2} and returns its JSON string representation using json.dumps.

import json

def word_counts_to_json(counts):
    # Return a JSON string for the counts dict
    pass
Exercise

Predict exactly what this script prints. Notice the indentation and the boolean value.

import json

report = {'ok': True, 'items': [1, 2]}
print(json.dumps(report, indent=2))
Exercise

Write build_report(total_words, unique_words, emails, dates) that returns a JSON string for a report dict with keys 'summary' (containing 'total_words' and 'unique_words'), 'entities' (containing 'emails' and 'dates'), and 'status' set to 'complete'. Use json.dumps with indent=2.

import json

def build_report(total_words, unique_words, emails, dates):
    # Build the report dict and return its JSON string with indent=2
    pass
Exercise

This function tries to serialize a report that contains a set, but json.dumps crashes with a TypeError. Fix it by converting the set to a list before serialization.

import json

def serialize_tags(tags):
    report = {'tags': tags, 'count': len(tags)}
    return json.dumps(report)
Exercise

Write analysis_to_json(word_count, email_list, date_list) that returns a JSON string for a complete analysis report. The report should have 'word_count' (the dict), 'entities' (a dict with 'emails' and 'dates'), and 'metadata' (a dict with 'version': '1.0' and 'total_entities': len(email_list) + len(date_list)). Use json.dumps with indent=2.

import json

def analysis_to_json(word_count, email_list, date_list):
    # Assemble the full report and return its JSON string
    pass

Recap

  • A report dict collects scattered findings into one predictable structure.
  • json.dumps serializes a dict to a string; indent makes it readable for humans.
  • Only strings, numbers, bools, lists, and dicts are JSON-safe — convert sets, tuples, and objects first.
  • Separate report building from analysis logic so each piece can be tested on its own.
  • Pretty output is for humans; compact output is for machines and networks.

Your capstone project now has all the pieces: ingestion, tokenization, statistics, pattern extraction, and reporting. The final lesson shows how to wire them together into a single command-line tool.

Checkpoint quiz

Which parameter makes json.dumps output human-readable with line breaks and indentation?

What happens if you pass a Python set to json.dumps?

Go deeper — technical resources