Programming with Python › Functions and Program Design › Day 60
Hands-on lab — Day 60: A Tour of the Standard Library
- ← Back to the Day 60 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library/
Commands
Setup
cd labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library
python3 --version Run
python3 examples/toolkit.py --dir sample-data
python3 examples/toolkit.py --dir sample-data --out report.json
cat report.json Test
bash tests/run_tests.sh File tree
examples/toolkit.py expected-output/FIELDS.md expected-output/sample-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md sample-data/a.csv sample-data/b.csv sample-data/notes.txt sample-data/sub/c.csv sample-data/x.json sample-data/y.json security.md starter/toolkit.py tests/run_tests.sh troubleshooting.md
Lab README
Day 060 lab — Stdlib Toolkit
Lesson
- Lesson title: A Tour of the Standard Library
- Day number: 60 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-060-a-tour-of-the-standard-library
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-060-a-tour-of-the-standard-librarywhen the site is running.
Purpose
Day 60's lesson tours the Python standard library — the "batteries included"
modules that ship with Python — and teaches the judgement to prefer them
before adding a third-party dependency. This lab makes that concrete. You
build Stdlib Toolkit, a small directory-audit tool that uses four drawers
of the toolbox together: pathlib to walk a folder, collections.Counter to
tally file extensions, datetime to stamp the report, and json to write it
out — proving you solved a genuine, useful task with nothing installed.
You build it from a starter, one exercise at a time, then run an automated
test suite that checks the tally deterministically (ignoring the volatile
timestamp). This is the everyday glue of AI work: before anything is trained,
you must describe your data directory precisely and reproducibly.
Learning objectives
- Walk a directory tree with
pathlib(Path,.rglob,.is_file) instead of gluing path strings together. - Tally categories with
collections.Counterand read the result largest-first with.most_common(). - Stamp a report with
datetime.now().isoformat()for a sortable, unambiguous timestamp. - Write and read structured results with
json, and keep the tool testable by taking all input from arguments (argparse). - Feel the standard-library-first habit directly: build a real tool with no
pip installat all.
Prerequisites
- The Day 60 lesson (read it first — it tours every module this lab uses).
- Days 57–59: functions and the
mainguard, modules and imports, and scope. - Day 56: reading and writing JSON with
json.load/json.dump, and command-line tools withargparse. - A text editor and a terminal. No experience beyond this course is assumed.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14.0).
- Linux — fully supported (any distribution with Python 3 and bash).
- Windows — use WSL and follow the Linux path, or substitute
pythonforpython3if that is how Python is exposed. The tool is pure standard-library Python and behaves identically everywhere.
Hardware requirements
Any computer that runs Python 3. The tool reads only file names and metadata and writes a tiny JSON report; it needs no special memory, disk, or GPU.
Required software
python3(3.8 or newer; tested on 3.14.0).bashfor the test runner (preinstalled on macOS and Linux).- Standard library only —
pathlib,collections,datetime,json,argparse, andsysall ship with Python. No packages to install. Seerequirements/README.md.
Free and open-source options
Everything here is free and open source: Python, bash, and the standard library. No account, API key, network access, or purchase is needed — which is the whole point of the lab. Every module it uses is part of Python itself.
Installation
None beyond Python itself. Move into this directory and you are ready:
cd labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library
python3 --version # confirm Python 3.8+ is available
File structure
day-060-a-tour-of-the-standard-library/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── sample-data/ ← bundled folder to audit (6 files, one nested)
│ ├── a.csv, b.csv
│ ├── sub/c.csv
│ ├── x.json, y.json
│ └── notes.txt
├── starter/
│ └── toolkit.py ← YOUR working file (4 numbered exercises)
├── examples/
│ └── toolkit.py ← complete reference implementation
├── tests/
│ └── run_tests.sh ← automated checks (deterministic tally, import)
├── expected-output/
│ ├── sample-run.txt ← real captured session with the reference
│ ├── test-run.txt ← real captured run of the test suite
│ └── FIELDS.md ← required behaviour on every platform
├── requirements/
│ └── README.md ← dependency statement (Python 3 only)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished tool first. Audit the bundled sample folder.
python3 examples/toolkit.py --dir sample-data
## 2. Write the same report to a file, then inspect it.
python3 examples/toolkit.py --dir sample-data --out report.json
cat report.json
## 3. Your task: complete the four exercises in the starter, then run it.
python3 starter/toolkit.py --dir sample-data
## 4. Prove a function is importable (the payoff of the main guard).
python3 -c "import sys; sys.path.insert(0, 'examples'); from toolkit import tally_extensions; print(tally_extensions(['a.csv', 'b.csv', 'c.txt']))"
## 5. Check your work, then clean up.
bash tests/run_tests.sh
rm -f report.json
What the commands do
--dir sample-data— walks the folder recursively withpathlib, tallies file extensions withcollections.Counter, stamps the moment withdatetime, and prints a JSON report. This is the four-drawer toolkit in one run.--out report.json— additionally writes the same report to a file withjson.dump, so you can save it and compare against a later run.cat report.json— shows the persisted, human-readable JSON report on disk.python3 -c "...tally_extensions..."— imports one function from the module and calls it without running the whole tool, which works only because the main guard holdsmainback on import.bash tests/run_tests.sh— builds a throwaway directory with a known mix of files (including an uppercase.CSVand a file with no extension), runs the reference tool, and asserts on the stable fields only — the volatile timestamp is deliberately ignored. Exits 0 only if every check passes.
Expected output
See expected-output/sample-run.txt — a
real captured session. The report for the bundled sample-data folder is:
{
"generated_at": "2026-07-13T11:56:58",
"directory": "sample-data",
"total_files": 6,
"by_extension": {
".csv": 3,
".json": 2,
".txt": 1
}
}
The generated_at timestamp is the current time and will differ on your
machine; every other field is deterministic.
expected-output/FIELDS.md lists the required
behaviour for every input on every platform.
Validation steps
python3 examples/toolkit.py --dir sample-dataprints a report withtotal_files: 6andby_extension{".csv": 3, ".json": 2, ".txt": 1}.python3 examples/toolkit.py --dir sample-data --out report.jsonthencat report.jsonshows the same report saved as valid JSON.- Complete the four exercises in
starter/toolkit.py, run it onsample-data, and confirm the tally matches the reference. python3 -c "...tally_extensions(['a.csv', 'b.csv', 'c.txt'])..."prints{'.csv': 2, '.txt': 1}.- Run the tests (next section) — every check must pass.
Tests
bash tests/run_tests.sh
Expected final line while the starter is unfinished: 14 checks, 0 failure(s). Once you complete all four starter exercises, the suite runs your
version through the same deterministic tally plus the main-guard check, giving
19 checks, 0 failure(s). The command exits 0 on success and non-zero on any
failure, so it can run in CI. A full captured run is in
expected-output/test-run.txt.
Cleanup
The tool writes only the report file you name with --out. Remove any you
created:
rm -f report.json my-report.json
To reset your work, restore the starter from git:
git checkout -- starter/toolkit.py. The test runner cleans up its own
temporary directory automatically.
Troubleshooting
See troubleshooting.md for the full list: python vs
python3, running from the lab directory, why the timestamp changes every
run, case-insensitive extension counting, files with no extension, importing
vs running, and permissions.
Security notes
See security.md. Short version: the tool makes no network
calls and needs no privileges; it reads only file names and metadata and
writes only the report you name. Preferring the standard library is itself a
security posture — fewer dependencies mean a smaller attack surface — and the
tool reads data with json, never eval().
Extension exercises
- Add a
--top Noption (withargparse) so the report includes atop_extensionslist of the N most common extensions, viaCounter.most_common(N). - Make the tool robust: if the
--dirdirectory does not exist, print a clear message to standard error and exit non-zero instead of crashing. - Add a
--formatoption (jsonortext) wheretextprints an aligned, human-readable one-line-per-extension summary. - Extend the report using two more drawers:
total_bytes(sump.stat().st_sizewithpathlib) andmean_bytes(withstatistics, guarding the empty-folder case). - Write your own
tests/test_toolkit.pythat importstally_extensionsand asserts its behaviour, including the case-insensitive and no-extension cases, printingall tests passedonly if every assertion holds.
Navigation
- Previous day: Day 59 — Scope and Namespaces
(
labs/sections/programming-with-python/day-059-scope-and-namespaces/). - Next day: Day 61 — Writing Readable Code
(
labs/sections/programming-with-python/day-061-writing-readable-code/, to be written). - Week 9 theme: Functions and Program Design — building, organising, and reaching for well-made code, of which the standard library is the largest and best-tested example.
Expected output
FIELDS.md
# Expected output — Day 060 lab
These are real captured runs from the authoring machine (macOS, Apple
Silicon, Python 3.14.0, bash 3.2, 2026-07-13). The tally is deterministic:
given the same directory contents, the tool produces the same counts on
every platform Python 3 runs on. The one field that changes between runs is
`generated_at`, because it is the current time — the tests deliberately
ignore it.
## Files
- `sample-run.txt` — the reference tool run against the bundled `sample-data`
folder: printed report, the same report written with `--out report.json`,
the file on disk, and a `python3 -c` import check of `tally_extensions`.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
still unfinished (14 checks, 0 failures). Absolute paths are shown as
`<repo>`; on your machine they are your real repository path.
## Required behaviour on every platform
The bundled `sample-data` folder holds six files — three `.csv` (one nested
in `sub/`), two `.json`, and one `.txt` — so a correct tool prints:
| Field | Value for `sample-data` | Notes |
| --- | --- | --- |
| `generated_at` | an ISO-8601 timestamp, e.g. `2026-07-13T11:56:58` | the current time; changes every run; not tested |
| `directory` | `sample-data` | the string passed to `--dir` |
| `total_files` | `6` | files found by walking the tree recursively |
| `by_extension` | `{".csv": 3, ".json": 2, ".txt": 1}` | ordered largest-first |
The test suite builds its own temporary tree with seven files — including one
uppercase `.CSV` and one file with no extension — and asserts:
| Behaviour | Expected |
| --- | --- |
| `total_files` | `7` |
| `.csv` count | `3` — `.CSV` and `.csv` count together (case-insensitive) |
| `.json` count | `2` |
| `.txt` count | `1` |
| no-extension file | counted under `(none)` (`1`), not dropped |
| ordering | `by_extension` is largest-count first (`.csv` before `.json`) |
| `--out FILE` | writes the same report as valid JSON to `FILE` |
## Platform notes
- The only visible difference between platforms is the shell prompt (`$`)
shown before each command; the program's own output is identical.
- `rglob("*")` walks subdirectories on every platform, so the nested
`sub/c.csv` is always found and counted.
- The test runner uses `mktemp -d` to build a throwaway directory tree and
removes it on exit. On Linux `mktemp -d -t toolkit-test.XXXXXX` behaves the
same as on macOS; the path is quoted and removed regardless.
- On Windows, run inside WSL and follow the Linux path; the tool is pure
standard-library Python and behaves identically.
sample-run.txt
$ python3 examples/toolkit.py --dir sample-data
{
"generated_at": "2026-07-13T11:56:58",
"directory": "sample-data",
"total_files": 6,
"by_extension": {
".csv": 3,
".json": 2,
".txt": 1
}
}
$ python3 examples/toolkit.py --dir sample-data --out report.json
{
"generated_at": "2026-07-13T11:56:58",
"directory": "sample-data",
"total_files": 6,
"by_extension": {
".csv": 3,
".json": 2,
".txt": 1
}
}
$ cat report.json
{
"generated_at": "2026-07-13T11:56:58",
"directory": "sample-data",
"total_files": 6,
"by_extension": {
".csv": 3,
".json": 2,
".txt": 1
}
}
$ python3 -c "import sys; sys.path.insert(0, 'examples'); from toolkit import tally_extensions; print(tally_extensions(['a.csv', 'b.csv', 'c.txt']))"
{'.csv': 2, '.txt': 1}
test-run.txt
Testing <repo>/labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library/examples/toolkit.py ...
ok: report is valid JSON, exit 0
ok: total_files is 7
ok: .csv counted 3 (case-insensitive)
ok: .json counted 2
ok: .txt counted 1
ok: no-extension file counted as (none)
ok: by_extension is largest-first
ok: --out writes a valid JSON report
Testing importability of examples/toolkit.py ...
ok: import tally_extensions computes correct tallies
Testing starter/toolkit.py ...
ok: starter is valid Python
Note: starter/toolkit.py still has unfinished exercises — testing structure only.
ok: starter defines walk_files
ok: starter defines tally_extensions
ok: starter defines build_report
ok: starter defines write_report
14 checks, 0 failure(s).
Source files
examples/toolkit.py (2867 bytes)
#!/usr/bin/env python3
"""toolkit.py — Stdlib Toolkit: a directory audit built from the standard
library only.
It walks a folder with pathlib, tallies file extensions with
collections.Counter, stamps the moment with datetime, and writes a JSON
report with json — four drawers of the standard-library toolbox, nothing
installed.
python3 examples/toolkit.py --dir sample-data
python3 examples/toolkit.py --dir sample-data --out report.json
"""
import argparse
import json
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path
def walk_files(directory):
"""Return a sorted list of every file under directory (recursively).
Uses pathlib: Path(directory).rglob("*") yields every entry in the tree,
and .is_file() keeps only files (not subdirectories).
"""
root = Path(directory)
return sorted(p for p in root.rglob("*") if p.is_file())
def tally_extensions(items):
"""Tally file extensions across items (paths or names).
Case-insensitive; a file with no extension is counted as "(none)".
Returns a plain dict ordered largest-count first.
"""
counter = Counter()
for item in items:
suffix = Path(item).suffix.lower() or "(none)"
counter[suffix] += 1
return dict(counter.most_common())
def build_report(directory, files):
"""Assemble the audit report dict from the walked files.
Stamps the current time with datetime and tallies extensions.
"""
return {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"directory": str(directory),
"total_files": len(files),
"by_extension": tally_extensions(files),
}
def write_report(path, report):
"""Write the report to path as pretty-printed JSON."""
with open(path, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2)
handle.write("\n")
def build_parser():
"""Build the argparse parser: --dir to audit, optional --out to save."""
parser = argparse.ArgumentParser(
prog="toolkit.py",
description="Audit a directory: count files by extension and stamp a JSON report.",
)
parser.add_argument(
"--dir",
required=True,
help="the directory to audit (walked recursively)",
)
parser.add_argument(
"--out",
default=None,
help="optional path to also write the JSON report to",
)
return parser
def main(argv):
"""Entry point: parse arguments, walk, build the report, print (and save)."""
parser = build_parser()
args = parser.parse_args(argv[1:])
files = walk_files(args.dir)
report = build_report(args.dir, files)
if args.out:
write_report(args.out, report)
print(json.dumps(report, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
metadata.yml (717 bytes)
lesson_id: D060
day: 60
kind: python-program
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library
- python3 --version
run_commands:
- python3 examples/toolkit.py --dir sample-data
- python3 examples/toolkit.py --dir sample-data --out report.json
- cat report.json
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- rm -f report.json my-report.json
- 'git checkout -- starter/toolkit.py # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-13'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh -> 14 checks, 0 failure(s), exit 0'
requirements/README.md (981 bytes)
# Dependencies — Day 060 lab
**Python 3 only. No third-party packages — that is the entire point of this lab.**
- `python3` (3.8 or newer; tested on 3.14.0). Preinstalled on most Linux
distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — specifically `pathlib`,
`collections`, `datetime`, `json`, `argparse`, and `sys`, all of which ship
with Python. There is deliberately no `requirements.txt`: this lab exists to
show that a real, useful directory-audit tool needs nothing installed at
all.
Check your Python is present and new enough:
```bash
python3 --version
```
If that prints `Python 3.8` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system. The tool itself is pure standard-library
Python and behaves identically everywhere.
sample-data/a.csv (21 bytes)
id,name
1,Ada
2,Alan
sample-data/b.csv (19 bytes)
id,value
1,10
2,20
sample-data/notes.txt (41 bytes)
Sample notes for the Stdlib Toolkit lab.
sample-data/sub/c.csv (24 bytes)
label,count
cat,3
dog,2
sample-data/x.json (31 bytes)
{"model": "demo", "epochs": 3}
sample-data/y.json (32 bytes)
{"split": "train", "size": 100}
starter/toolkit.py (3583 bytes)
#!/usr/bin/env python3
"""toolkit.py — YOUR working file.
Build the Stdlib Toolkit one exercise at a time, using the standard library
only. Each numbered exercise names exactly what to write. The finished
reference is in examples/toolkit.py — try each exercise yourself before
peeking.
When all four exercises are done, this file behaves like the reference:
python3 starter/toolkit.py --dir sample-data
python3 starter/toolkit.py --dir sample-data --out report.json
Then run: bash tests/run_tests.sh
"""
import argparse
import json
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path
def walk_files(directory):
"""Return a sorted list of every file under directory (recursively)."""
# Exercise 1: WALK THE DIRECTORY (pathlib).
# 1. Make a Path from directory.
# 2. Use .rglob("*") to yield every entry in the tree.
# 3. Keep only the ones where .is_file() is True.
# 4. Return them as a sorted list.
# Hint: return sorted(p for p in Path(directory).rglob("*") if p.is_file())
raise NotImplementedError("Exercise 1: implement walk_files")
def tally_extensions(items):
"""Tally file extensions across items (paths or names).
Case-insensitive; a file with no extension is counted as "(none)".
Returns a plain dict ordered largest-count first.
"""
# Exercise 2: TALLY EXTENSIONS (collections.Counter).
# 1. Make a Counter.
# 2. For each item, take Path(item).suffix.lower(); if it is empty,
# use the string "(none)" instead.
# 3. Add one to the counter for that suffix.
# 4. Return dict(counter.most_common()) so it is ordered largest first.
raise NotImplementedError("Exercise 2: implement tally_extensions")
def build_report(directory, files):
"""Assemble the audit report dict from the walked files."""
# Exercise 3: BUILD THE STAMPED REPORT (datetime + your tally).
# Return a dict with these keys:
# "generated_at": datetime.now().isoformat(timespec="seconds")
# "directory": str(directory)
# "total_files": len(files)
# "by_extension": tally_extensions(files)
raise NotImplementedError("Exercise 3: implement build_report")
def write_report(path, report):
"""Write the report to path as pretty-printed JSON."""
# Exercise 4: WRITE THE REPORT AS JSON (json).
# 1. Open path for writing (encoding="utf-8").
# 2. Use json.dump(report, handle, indent=2) to write it.
# 3. Write a trailing newline so the file ends cleanly.
raise NotImplementedError("Exercise 4: implement write_report")
def build_parser():
"""Build the argparse parser: --dir to audit, optional --out to save. (Provided.)"""
parser = argparse.ArgumentParser(
prog="toolkit.py",
description="Audit a directory: count files by extension and stamp a JSON report.",
)
parser.add_argument(
"--dir",
required=True,
help="the directory to audit (walked recursively)",
)
parser.add_argument(
"--out",
default=None,
help="optional path to also write the JSON report to",
)
return parser
def main(argv):
"""Entry point: parse arguments, walk, build the report, print (and save). (Provided.)"""
parser = build_parser()
args = parser.parse_args(argv[1:])
files = walk_files(args.dir)
report = build_report(args.dir, files)
if args.out:
write_report(args.out, report)
print(json.dumps(report, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
tests/run_tests.sh (5822 bytes)
#!/usr/bin/env bash
# Tests for the Day 060 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Exercises the reference Stdlib Toolkit (examples/toolkit.py) against a
# freshly built temporary directory with a KNOWN mix of files, so the run is
# deterministic. Every check parses the JSON report and asserts on the stable
# fields (total_files, by_extension, directory); the volatile "generated_at"
# timestamp is deliberately NOT checked. It then imports tally_extensions to
# check its return value directly, and finally checks the learner's starter —
# structurally while exercises are unfinished, and to the same strict standard
# once they are complete. No network, non-interactive. Exits 0 only if every
# check passes.
set -u
# Keep the working tree clean: do not let imported modules write __pycache__.
export PYTHONDONTWRITEBYTECODE=1
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/toolkit.py"
starter="${lab_dir}/starter/toolkit.py"
failures=0
checks=0
# A fresh temporary directory tree per run; removed on exit.
tmp="$(mktemp -d -t toolkit-test.XXXXXX)"
cleanup() { rm -rf "${tmp}"; }
trap cleanup EXIT
# Build a KNOWN tree: 3 csv (one nested), 2 json, 1 txt, 1 no-extension,
# and one .CSV to prove the tally is case-insensitive.
mkdir -p "${tmp}/data/sub"
printf 'a\n' > "${tmp}/data/a.csv"
printf 'b\n' > "${tmp}/data/b.csv"
printf 'c\n' > "${tmp}/data/sub/c.CSV" # uppercase on purpose
printf '{}\n' > "${tmp}/data/x.json"
printf '{}\n' > "${tmp}/data/y.json"
printf 'note\n' > "${tmp}/data/notes.txt"
printf 'readme\n' > "${tmp}/data/README" # no extension -> "(none)"
# Expected: 7 files; .csv=3 (case-insensitive), .json=2, .txt=1, (none)=1
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# field <json-text> <python-expr-on-d> : print the value of a report field.
field() {
printf '%s' "$1" | python3 -c "import sys, json; d = json.load(sys.stdin); print($2)"
}
run_cli_checks() {
local script="$1"
echo "Testing ${script} ..."
local out
out="$(python3 "${script}" --dir "${tmp}/data")"
local code=$?
# Valid JSON and exit 0.
if [ "${code}" -eq 0 ] && printf '%s' "${out}" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null; then
check "report is valid JSON, exit 0" "yes"
else
check "report is valid JSON, exit 0" "no"
echo " (exit ${code}; output: ${out})"
return
fi
# Deterministic fields.
[ "$(field "${out}" 'd["total_files"]')" = "7" ] \
&& check "total_files is 7" "yes" || check "total_files is 7" "no"
[ "$(field "${out}" 'd["by_extension"][".csv"]')" = "3" ] \
&& check ".csv counted 3 (case-insensitive)" "yes" || check ".csv counted 3 (case-insensitive)" "no"
[ "$(field "${out}" 'd["by_extension"][".json"]')" = "2" ] \
&& check ".json counted 2" "yes" || check ".json counted 2" "no"
[ "$(field "${out}" 'd["by_extension"][".txt"]')" = "1" ] \
&& check ".txt counted 1" "yes" || check ".txt counted 1" "no"
[ "$(field "${out}" 'd["by_extension"]["(none)"]')" = "1" ] \
&& check "no-extension file counted as (none)" "yes" || check "no-extension file counted as (none)" "no"
# Tally is ordered largest-first (.csv before .json before .txt).
[ "$(field "${out}" 'list(d["by_extension"])[0]')" = ".csv" ] \
&& check "by_extension is largest-first" "yes" || check "by_extension is largest-first" "no"
# --out writes a valid JSON file with the same total.
local outfile="${tmp}/report.json"
rm -f "${outfile}"
python3 "${script}" --dir "${tmp}/data" --out "${outfile}" >/dev/null
if [ -f "${outfile}" ] && python3 -c "import json; d = json.load(open('${outfile}')); assert d['total_files'] == 7" 2>/dev/null; then
check "--out writes a valid JSON report" "yes"
else
check "--out writes a valid JSON report" "no"
fi
rm -f "${outfile}"
}
# --- Reference toolkit: always tested strictly ---
run_cli_checks "${ref}"
# --- Import tally_extensions and check return values ---
echo "Testing importability of examples/toolkit.py ..."
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from toolkit import tally_extensions; \
assert tally_extensions(['a.csv', 'b.csv', 'c.txt']) == {'.csv': 2, '.txt': 1}; \
assert tally_extensions(['A.CSV', 'b.csv']) == {'.csv': 2}; \
assert tally_extensions(['README']) == {'(none)': 1}"; then
check "import tally_extensions computes correct tallies" "yes"
else
check "import tally_extensions computes correct tallies" "no"
fi
# --- Learner starter ---
echo "Testing starter/toolkit.py ..."
if python3 -c "compile(open('${starter}').read(), '${starter}', 'exec')" 2>/dev/null; then
check "starter is valid Python" "yes"
else
check "starter is valid Python" "no"
fi
if grep -q 'NotImplementedError' "${starter}"; then
echo "Note: starter/toolkit.py still has unfinished exercises — testing structure only."
grep -q 'def walk_files' "${starter}" && check "starter defines walk_files" "yes" || check "starter defines walk_files" "no"
grep -q 'def tally_extensions' "${starter}" && check "starter defines tally_extensions" "yes" || check "starter defines tally_extensions" "no"
grep -q 'def build_report' "${starter}" && check "starter defines build_report" "yes" || check "starter defines build_report" "no"
grep -q 'def write_report' "${starter}" && check "starter defines write_report" "yes" || check "starter defines write_report" "no"
else
run_cli_checks "${starter}"
grep -q '__name__ == "__main__"' "${starter}" && check "starter has the main guard" "yes" || check "starter has the main guard" "no"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 060 lab
python: command not found
Use python3 explicitly, as every command in this lab does. On macOS and
most Linux systems, bare python may be missing or point to an old version.
Check with python3 --version.
No such file or directory for sample-data
Run the commands from the lab directory — the folder that contains
examples/, starter/, and sample-data/. If you are in the repository
root, cd into the lab first:
cd labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library
python3 examples/toolkit.py --dir sample-data
The starter raises NotImplementedError when I run it
That is expected until you finish the exercises. Each unfinished function
raises NotImplementedError on purpose so you cannot mistake an empty
function for a working one. Replace each raise NotImplementedError(...)
line with the real body described in the comment above it. Once all four
exercises are done, the file behaves like the reference.
toolkit.py: error: the following arguments are required: --dir
That message comes from argparse itself: --dir is required, so you must
say which directory to audit. Argparse prints the usage line and exits with
code 2, its own convention for usage errors. Add --dir sample-data.
The generated_at timestamp is different every time I run it
That is correct, not a bug: datetime.now() returns the current moment, so
generated_at changes on every run. This is exactly why the tests ignore it
and check only the stable fields (total_files, by_extension,
directory). When you compare two reports, ignore this field or compare the
tallies directly.
.CSV and .csv are counted as two different types
You are tallying the suffix without lowercasing it. Use
Path(item).suffix.lower() so .CSV, .Csv, and .csv all count together.
The reference does this, and the test suite checks it with a deliberately
uppercase .CSV file.
A file with no extension disappears from the tally
Path("README").suffix is an empty string "", and an empty key is easy to
lose. Map the empty suffix to a visible label — the reference uses
Path(item).suffix.lower() or "(none)" — so files without an extension are
still counted.
ModuleNotFoundError: No module named 'toolkit' in the import check
Python looks for modules on its search path (sys.path), which does not
include the examples/ subfolder by default. The import one-liner adds it
first:
python3 -c "import sys; sys.path.insert(0, 'examples'); from toolkit import tally_extensions; print(tally_extensions(['a.csv', 'b.csv']))"
Run it from the lab directory (the folder that contains examples/).
bash: tests/run_tests.sh: Permission denied
Run it through bash explicitly, as the README shows: bash tests/run_tests.sh.
You do not need to chmod +x anything.
Security notes
Security notes — Day 060 lab
-
What the tool does: reads the directory you name with
--dir, reads only file names and metadata (it never opens or reads file contents), and prints or writes a small JSON report. It makes no network connections, needs no privileges, and writes only the report file you name with--out. The test runner builds a throwaway directory withmktemp -dand removes it on exit. -
Fewer dependencies is a security posture. The lab's whole lesson is standard-library-first, and that habit is also a security habit: every third-party package you install is code from someone else that runs with your program's privileges, and the package supply chain has been attacked through malicious or hijacked releases. A tool that uses only the standard library has a far smaller attack surface — there is nothing extra to trust, update, or be compromised through.
-
Read data with
json, nevereval(). When the tool reads a JSON report back, it usesjson.load, which turns text into plain Python values and can never execute code. Do not reach foreval()/exec()to "parse" a report or any file contents; those run the string as Python, so a malicious value could delete files or open a network connection. -
Treat directory input as untrusted. The tool walks whatever path you give
--dir. Point it at your own project folders, not at system directories you do not control. It only reads metadata, but it is good practice to know exactly what you are pointing a file-walking tool at. -
Fail loudly, not silently. Every command prints its result and returns an exit code, so a person or a script can tell whether it worked. When you build the extension challenge (a missing
--dirprinting to standard error and exiting non-zero), keep that discipline — a clear error beats a silent wrong answer. -
Reading before running: every file in this lab is short and commented. Read
examples/toolkit.pyandtests/run_tests.shbefore running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.