JSON CLI Tool
Build a command-line tool that reads a JSON array of developer objects from stdin
and prints a human-readable summary. It’s your “Hello World” for modern Python —
but actually useful, and it exercises a real uv project end to end: project init,
the standard library, dataclasses, stdin, and a few collection idioms.
What you’ll practice
Section titled “What you’ll practice”- Creating a
uvproject from scratch (uv init,uv run) - Reading stdin, writing stdout
- Modeling data with dataclasses
- Parsing JSON with the stdlib
jsonmodule (no third-party deps) collections.defaultdict,statistics.mean,min/maxwith a key, f-strings- Running
ruffandtyover your code
Requirements
Section titled “Requirements”- Read a JSON array of objects from stdin.
- Parse the JSON into typed
dataclassinstances. - Print a formatted summary to stdout: total count, a per-language breakdown with average years of experience, and the most/least experienced developer.
Example input
Section titled “Example input”Pipe this to stdin:
[ {"name": "Alice", "language": "Kotlin", "years": 3}, {"name": "Bob", "language": "TypeScript", "years": 5}, {"name": "Charlie", "language": "Go", "years": 2}, {"name": "Diana", "language": "Kotlin", "years": 1}, {"name": "Eve", "language": "TypeScript", "years": 4}]Expected output
Section titled “Expected output”=== Developer Summary ===Total developers: 5
By language: Go: 1 developers (avg 2.0 years) Kotlin: 2 developers (avg 2.0 years) TypeScript: 2 developers (avg 4.5 years)
Most experienced: Bob (TypeScript, 5 years)Least experienced: Diana (Kotlin, 1 years)The worked solution
Section titled “The worked solution”A uv project with a single module does the whole job:
Directoryjson-cli/
- pyproject.toml deps + metadata
- uv.lock lockfile
- .python-version pinned interpreter
Directorysrc/
Directoryjsoncli/
- __init__.py
- main.py the entire program
Scaffold it:
uv init --package --app json-clicd json-cliuv add --dev ruff ty # no runtime deps — stdlib onlypyproject.toml
Section titled “pyproject.toml”No runtime dependencies — everything we need is in the standard library. We add a script entry point so the tool can be invoked by name, plus dev tooling.
[project]name = "json-cli"version = "0.1.0"description = "Summarize a JSON array of developers from stdin"readme = "README.md"requires-python = ">=3.13"dependencies = [] # stdlib only
[project.scripts]json-cli = "jsoncli.main:main" # creates a `json-cli` command
[dependency-groups]dev = [ "ruff>=0.6", "ty",]
[tool.ruff.lint]select = ["E", "F", "I", "UP", "B"]
[build-system]requires = ["uv_build>=0.7"]build-backend = "uv_build"main.py
Section titled “main.py”The whole program. A few things to notice if you’re coming from TS/Go:
@dataclassturns a class into a plain data holder — auto-generated__init__,__repr__, and equality. It’s the Pythoninterface/type(TS) orstruct(Go) for plain data.json.loadshands usdicts; we splat each into the dataclass with**raw.sys.stdin.read()reads all of stdin in one go — the idiomatic “slurp stdin,” likeio.ReadAll(os.Stdin)in Go.defaultdict(list)is “group by” without the key-existence dance, andstatistics.meanis the average.min/maxtake akey=function, likeslices.MaxFuncin Go orMath.max(...arr.map(...))in TS but cleaner.- f-strings (
f"{name}: {n} developers") are JS template literals;f"{avg:.1f}"formats to one decimal place.
import jsonimport sysfrom collections import defaultdictfrom dataclasses import dataclassfrom statistics import mean
@dataclassclass Developer: name: str language: str years: int
def main() -> None: raw_input = sys.stdin.read()
if not raw_input.strip(): print("Error: No input provided. Pipe JSON to stdin.", file=sys.stderr) sys.exit(1)
try: records = json.loads(raw_input) developers = [Developer(**rec) for rec in records] except (json.JSONDecodeError, TypeError) as exc: print(f"Error parsing JSON: {exc}", file=sys.stderr) sys.exit(1)
if not developers: print("No developers found in input.") return
print("=== Developer Summary ===") print(f"Total developers: {len(developers)}") print()
# Group by language, then report the count and average years for each. by_language: dict[str, list[Developer]] = defaultdict(list) for dev in developers: by_language[dev.language].append(dev)
print("By language:") for language in sorted(by_language): devs = by_language[language] avg_years = mean(d.years for d in devs) print(f" {language}: {len(devs)} developers (avg {avg_years:.1f} years)") print()
# Find the extremes with a key function. most = max(developers, key=lambda d: d.years) least = min(developers, key=lambda d: d.years)
print(f"Most experienced: {most.name} ({most.language}, {most.years} years)") print(f"Least experienced: {least.name} ({least.language}, {least.years} years)")
if __name__ == "__main__": main()Run it
Section titled “Run it”-
Lint, format, and type-check (the habit to build from day one):
Terminal window uv run ruff format .uv run ruff check .uv run ty check -
Run with sample data piped to stdin:
Terminal window echo '[{"name":"Alice","language":"Kotlin","years":3},{"name":"Bob","language":"TypeScript","years":5}]' \| uv run json-cli -
Or pipe a file (save the example input as
sample.json):Terminal window uv run json-cli < sample.json# equivalently: cat sample.json | uv run python -m jsoncli.main