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
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.
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
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
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.
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
json.dumps turns a Python dict into a JSON string.
Key order follows dict insertion order in Python 3.7+.
import json
def word_counts_to_json(counts):
return json.dumps(counts)
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))
indent=2 adds two spaces per nesting level.
Python's True becomes JSON's lowercase true.
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
Nest dicts inside dicts to create the required structure.
json.dumps with indent=2 formats the output; parse it back in tests to avoid brittle string matching.
import json
def build_report(total_words, unique_words, emails, dates):
report = {
'summary': {
'total_words': total_words,
'unique_words': unique_words
},
'entities': {
'emails': emails,
'dates': dates
},
'status': 'complete'
}
return json.dumps(report, indent=2)
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)
list(tags) converts a set into a list, which json.dumps accepts.
The conversion must happen before json.dumps sees the value.
import json
def serialize_tags(tags):
report = {'tags': list(tags), 'count': len(tags)}
return json.dumps(report)
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
Build the nested dict first, then pass it to json.dumps with indent=2.
len(email_list) + len(date_list) gives the total entity count.
import json
def analysis_to_json(word_count, email_list, date_list):
report = {
'word_count': word_count,
'entities': {
'emails': email_list,
'dates': date_list
},
'metadata': {
'version': '1.0',
'total_entities': len(email_list) + len(date_list)
}
}
return json.dumps(report, indent=2)
Recap
- A report dict collects scattered findings into one predictable structure.
json.dumpsserializes a dict to a string;indentmakes 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.